Compare commits

...

11 Commits

103 changed files with 817 additions and 247 deletions

View File

@ -0,0 +1,40 @@
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace OliverBooth.Api;
internal sealed class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
: IConfigureNamedOptions<SwaggerGenOptions>
{
public void Configure(SwaggerGenOptions options)
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateVersionInfo(description));
}
}
public void Configure(string? name, SwaggerGenOptions options)
{
Configure(options);
}
private OpenApiInfo CreateVersionInfo(
ApiVersionDescription description)
{
var info = new OpenApiInfo
{
Title = "api.oliverbooth.dev",
Version = description.ApiVersion.ToString()
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}

View File

@ -1,42 +1,54 @@
using Asp.Versioning;
using Humanizer;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Blog;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Api.Data;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Data.Web.Users;
using OliverBooth.Common.Services;
namespace OliverBooth.Controllers.Api.v1;
namespace OliverBooth.Api.Controllers.v1.Blog;
/// <summary>
/// Represents a controller for the blog API.
/// </summary>
[ApiController]
[Route("api/v{version:apiVersion}/blog")]
[Route("blog")]
[Produces("application/json")]
[ApiVersion(1)]
public sealed class BlogApiController : ControllerBase
[Obsolete("API v1 is deprecated and will be removed in future. Use /v2")]
public sealed class BlogController : ControllerBase
{
private readonly IBlogPostService _blogPostService;
private readonly IUserService _userService;
/// <summary>
/// Initializes a new instance of the <see cref="BlogApiController" /> class.
/// Initializes a new instance of the <see cref="BlogController" /> class.
/// </summary>
/// <param name="blogPostService">The <see cref="IBlogPostService" />.</param>
/// <param name="userService">The <see cref="IUserService" />.</param>
public BlogApiController(IBlogPostService blogPostService, IUserService userService)
public BlogController(IBlogPostService blogPostService, IUserService userService)
{
_blogPostService = blogPostService;
_userService = userService;
}
[Route("count")]
/// <summary>
/// Returns the number of publicly published blog posts.
/// </summary>
/// <returns>The number of publicly published blog posts.</returns>
[HttpGet("count")]
[EndpointDescription("Returns the number of publicly published blog posts.")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Count()
{
return Ok(new { count = _blogPostService.GetBlogPostCount() });
}
/// <summary>
/// Returns a collection of all blog posts on the specified page.
/// </summary>
/// <param name="page">The page number.</param>
/// <returns>An array of <see cref="IBlogPost" /> objects.</returns>
[HttpGet("posts/{page:int?}")]
[EndpointDescription("Returns a collection of all blog posts on the specified page.")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BlogPost[]))]
public IActionResult GetAllBlogPosts(int page = 0)
{
const int itemsPerPage = 10;
@ -44,7 +56,15 @@ public sealed class BlogApiController : ControllerBase
return Ok(allPosts.Select(post => CreatePostObject(post)));
}
/// <summary>
/// Returns a collection of all blog posts which contain the specified tag on the specified page.
/// </summary>
/// <param name="tag">The tag for which to search.</param>
/// <param name="page">The page number.</param>
/// <returns>An array of <see cref="IBlogPost" /> objects.</returns>
[HttpGet("posts/tagged/{tag}/{page:int?}")]
[EndpointDescription("Returns a collection of all blog posts which contain the specified tag on the specified page.")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BlogPost[]))]
public IActionResult GetTaggedBlogPosts(string tag, int page = 0)
{
const int itemsPerPage = 10;
@ -55,13 +75,18 @@ public sealed class BlogApiController : ControllerBase
return Ok(allPosts.Select(post => CreatePostObject(post)));
}
/// <summary>
/// Returns an object representing the author with the specified ID.
/// </summary>
/// <param name="id">The ID of the author.</param>
/// <returns>An object representing the author.</returns>
[HttpGet("author/{id:guid}")]
[EndpointDescription("Returns an object representing the author with the specified ID.")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Author))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetAuthor(Guid id)
{
if (!_userService.TryGetUser(id, out IUser? author))
{
return NotFound();
}
if (!_userService.TryGetUser(id, out IUser? author)) return NotFound();
return Ok(new
{
@ -71,14 +96,18 @@ public sealed class BlogApiController : ControllerBase
});
}
/// <summary>
/// Returns an object representing the blog post with the specified ID.
/// </summary>
/// <param name="id">The ID of the blog post.</param>
/// <returns>An object representing the blog post.</returns>
[HttpGet("post/{id:guid?}")]
[EndpointDescription("Returns an object representing the blog post with the specified ID.")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BlogPost))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetPost(Guid id)
{
if (!_blogPostService.TryGetPost(id, out IBlogPost? post))
{
return NotFound();
}
if (!_blogPostService.TryGetPost(id, out IBlogPost? post)) return NotFound();
return Ok(CreatePostObject(post, true));
}

View File

@ -0,0 +1,47 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Api.Data;
using OliverBooth.Common.Data.Web.Users;
using OliverBooth.Common.Services;
namespace OliverBooth.Api.Controllers.v2.Blog;
/// <summary>
/// Represents an API controller which allows reading authors of blog posts.
/// </summary>
[ApiController]
[Route("v{version:apiVersion}/blog/author")]
[Produces("application/json")]
[ApiVersion(2)]
public sealed class AuthorController : ControllerBase
{
private readonly IUserService _userService;
/// <summary>
/// Initializes a new instance of the <see cref="AuthorController" /> class.
/// </summary>
/// <param name="userService">The <see cref="IUserService" />.</param>
public AuthorController(IUserService userService)
{
_userService = userService;
}
/// <summary>
/// Returns an object representing the author with the specified ID.
/// </summary>
/// <param name="id">The ID of the author.</param>
/// <returns>An object representing the author.</returns>
[HttpGet("{id:guid}")]
[EndpointDescription("Returns an object representing the author with the specified ID.")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Author))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetAuthor(Guid id)
{
if (!_userService.TryGetUser(id, out IUser? author))
{
return NotFound();
}
return Ok(Author.FromUser(author));
}
}

View File

@ -0,0 +1,92 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Api.Data;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
namespace OliverBooth.Api.Controllers.v2.Blog;
/// <summary>
/// Represents an API controller which allows reading and writing of blog posts.
/// </summary>
[ApiController]
[Route("v{version:apiVersion}/blog/post")]
[Produces("application/json")]
[ApiVersion(2)]
public sealed class PostController : ControllerBase
{
private const int ItemsPerPage = 10;
private readonly IBlogPostService _blogPostService;
/// <summary>
/// Initializes a new instance of the <see cref="PostController" /> class.
/// </summary>
/// <param name="blogPostService">The <see cref="IBlogPostService" />.</param>
public PostController(IBlogPostService blogPostService)
{
_blogPostService = blogPostService;
}
/// <summary>
/// Returns a collection of all blog posts on the specified page.
/// </summary>
/// <param name="page">The page number.</param>
/// <returns>An array of <see cref="IBlogPost" /> objects.</returns>
[HttpGet("all/{page:int?}")]
[EndpointDescription("Returns a collection of all blog posts on the specified page.")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BlogPost[]))]
public IActionResult All(int page = 0)
{
IReadOnlyList<IBlogPost> allPosts = _blogPostService.GetBlogPosts(page, ItemsPerPage);
return Ok(allPosts.Select(post => BlogPost.FromBlogPost(post, _blogPostService)));
}
/// <summary>
/// Returns the number of publicly published blog posts.
/// </summary>
/// <returns>The number of publicly published blog posts.</returns>
[HttpGet("count")]
[EndpointDescription("Returns the number of publicly published blog posts.")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Count()
{
return Ok(new { count = _blogPostService.GetBlogPostCount() });
}
/// <summary>
/// Returns an object representing the blog post with the specified ID.
/// </summary>
/// <param name="id">The ID of the blog post.</param>
/// <returns>An object representing the blog post.</returns>
[HttpGet("{id:guid}")]
[EndpointDescription("Returns an object representing the blog post with the specified ID.")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BlogPost))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetPost(Guid id)
{
if (!_blogPostService.TryGetPost(id, out IBlogPost? post))
{
return NotFound();
}
return Ok(BlogPost.FromBlogPost(post, _blogPostService, true));
}
/// <summary>
/// Returns a collection of all blog posts which contain the specified tag on the specified page.
/// </summary>
/// <param name="tag">The tag for which to search.</param>
/// <param name="page">The page number.</param>
/// <returns>An array of <see cref="IBlogPost" /> objects.</returns>
[HttpGet("tagged/{tag}/{page:int?}")]
[EndpointDescription("Returns a collection of all blog posts which contain the specified tag on the specified page.")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BlogPost[]))]
public IActionResult Tagged(string tag, int page = 0)
{
tag = tag.Replace('-', ' ').ToLowerInvariant();
IReadOnlyList<IBlogPost> allPosts = _blogPostService.GetBlogPosts(page, ItemsPerPage);
allPosts = allPosts.Where(post => post.Tags.Contains(tag)).ToList();
return Ok(allPosts.Select(post => BlogPost.FromBlogPost(post, _blogPostService)));
}
}

View File

@ -0,0 +1,26 @@
using System.Text.Json.Serialization;
using OliverBooth.Common.Data.Web.Users;
namespace OliverBooth.Api.Data;
internal sealed class Author
{
[JsonPropertyName("avatarUrl"), JsonInclude, JsonPropertyOrder(2)]
public Uri AvatarUrl { get; private set; } = null!;
[JsonPropertyName("id"), JsonInclude, JsonPropertyOrder(0)]
public Guid Id { get; private set; }
[JsonPropertyName("name"), JsonInclude, JsonPropertyOrder(1)]
public string Name { get; private set; } = string.Empty;
public static Author FromUser(IUser author)
{
return new Author
{
Id = author.Id,
Name = author.DisplayName,
AvatarUrl = author.AvatarUrl
};
}
}

View File

@ -0,0 +1,83 @@
using System.Text.Json.Serialization;
using Humanizer;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
namespace OliverBooth.Api.Data;
internal sealed class BlogPost
{
[JsonPropertyName("author"), JsonInclude, JsonPropertyOrder(3)]
public Guid Author { get; private set; }
[JsonPropertyName("commentsEnabled"), JsonInclude, JsonPropertyOrder(1)]
public bool CommentsEnabled { get; private set; }
[JsonPropertyName("content"), JsonInclude, JsonPropertyOrder(11)]
public string? Content { get; private set; }
[JsonPropertyName("excerpt"), JsonInclude, JsonPropertyOrder(10)]
public string Excerpt { get; private set; } = string.Empty;
[JsonPropertyName("formattedPublishDate"), JsonInclude, JsonPropertyOrder(7)]
public string FormattedPublishDate { get; private set; } = string.Empty;
[JsonPropertyName("formattedUpdateDate"), JsonInclude, JsonPropertyOrder(8)]
public string? FormattedUpdateDate { get; private set; }
[JsonPropertyName("humanizedTimestamp"), JsonInclude, JsonPropertyOrder(9)]
public string HumanizedTimestamp { get; private set; } = string.Empty;
[JsonPropertyName("id"), JsonInclude, JsonPropertyOrder(0)]
public Guid Id { get; private set; }
[JsonPropertyName("identifier"), JsonInclude, JsonPropertyOrder(2)]
public string Identifier { get; private set; } = string.Empty;
[JsonPropertyName("trimmed"), JsonInclude, JsonPropertyOrder(12)]
public bool IsTrimmed { get; private set; }
[JsonPropertyName("published"), JsonInclude, JsonPropertyOrder(5)]
public long Published { get; private set; }
[JsonPropertyName("tags"), JsonInclude, JsonPropertyOrder(13)]
public IEnumerable<string> Tags { get; private set; } = ArraySegment<string>.Empty;
[JsonPropertyName("title"), JsonInclude, JsonPropertyOrder(4)]
public string Title { get; private set; } = string.Empty;
[JsonPropertyName("updated"), JsonInclude, JsonPropertyOrder(6)]
public long? Updated { get; private set; }
[JsonPropertyName("url"), JsonInclude, JsonPropertyOrder(14)]
public object Url { get; private set; } = null!;
public static BlogPost FromBlogPost(IBlogPost post, IBlogPostService blogPostService,
bool includeContent = false)
{
return new()
{
Id = post.Id,
CommentsEnabled = post.EnableComments,
Identifier = post.GetDisqusIdentifier(),
Author = post.Author.Id,
Title = post.Title,
Published = post.Published.ToUnixTimeSeconds(),
Updated = post.Updated?.ToUnixTimeSeconds(),
FormattedPublishDate = post.Published.ToString("dddd, d MMMM yyyy HH:mm"),
FormattedUpdateDate = post.Updated?.ToString("dddd, d MMMM yyyy HH:mm"),
HumanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(),
Excerpt = blogPostService.RenderExcerpt(post, out bool trimmed),
Content = includeContent ? blogPostService.RenderPost(post) : null,
IsTrimmed = trimmed,
Tags = post.Tags.Select(t => t.Replace(' ', '-')),
Url = new
{
year = post.Published.ToString("yyyy"),
month = post.Published.ToString("MM"),
day = post.Published.ToString("dd"),
slug = post.Slug
}
};
}
}

View File

@ -0,0 +1,22 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["OliverBooth.Api/OliverBooth.Api.csproj", "OliverBooth.Api/"]
COPY . .
WORKDIR "/src/OliverBooth.Api"
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "OliverBooth.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OliverBooth.Api.dll"]

View File

@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<VersionPrefix>2.0.0</VersionPrefix>
</PropertyGroup>
<PropertyGroup Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' == ''">
<Version>$(VersionPrefix)-$(VersionSuffix)</Version>
<AssemblyVersion>$(VersionPrefix).0</AssemblyVersion>
<FileVersion>$(VersionPrefix).0</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">
<Version>$(VersionPrefix)-$(VersionSuffix).$(BuildNumber)</Version>
<AssemblyVersion>$(VersionPrefix).$(BuildNumber)</AssemblyVersion>
<FileVersion>$(VersionPrefix).$(BuildNumber)</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(VersionSuffix)' == ''">
<Version>$(VersionPrefix)</Version>
<AssemblyVersion>$(VersionPrefix).0</AssemblyVersion>
<FileVersion>$(VersionPrefix).0</FileVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OliverBooth.Common\OliverBooth.Common.csproj"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
@OliverBooth.Api_HostAddress = https://localhost:2844
@TestPostId = bdcd9789-f0a6-4721-a66d-b6f7e2acd0f7
GET {{OliverBooth.Api_HostAddress}}/v1/blog/post/{{TestPostId}}
Accept: application/json
###

View File

@ -0,0 +1,83 @@
using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using OliverBooth.Api;
using OliverBooth.Common.Extensions;
using Serilog;
using Swashbuckle.AspNetCore.SwaggerGen;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/latest.log", rollingInterval: RollingInterval.Day)
#if DEBUG
.MinimumLevel.Debug()
#endif
.CreateLogger();
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddTomlFile("data/config.toml", true, true);
builder.Logging.ClearProviders();
builder.Logging.AddSerilog();
builder.Services.AddCors(options =>
{
options.AddPolicy("localhost", policy => policy.WithOrigins("https://localhost:2845"));
options.AddPolicy("site", policy => policy.WithOrigins("https://oliverbooth.dev"));
});
builder.Services.AddCommonServices();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => options.ResolveConflictingActions(resolver =>
{
foreach (ApiDescription description in resolver)
{
if (description.GetApiVersion()?.MajorVersion == 2)
{
return description;
}
}
return null;
}));
builder.Services.AddControllers();
builder.Services.AddRouting(options => options.LowercaseUrls = true);
builder.Services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(2);
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();
if (builder.Environment.IsProduction())
{
builder.WebHost.AddCertificateFromEnvironment(2844, 5048);
}
WebApplication app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (ApiVersionDescription description in provider.ApiVersionDescriptions)
{
var url = $"/swagger/{description.GroupName}/swagger.json";
options.SwaggerEndpoint(url, description.GroupName.ToUpperInvariant());
}
});
}
app.UseHttpsRedirection();
app.MapControllers();
app.UseCors(app.Environment.IsDevelopment() ? "localhost" : "site");
app.Run();

View File

@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Blog.Configuration;
using Microsoft.Extensions.Configuration;
using OliverBooth.Common.Data.Blog.Configuration;
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <summary>
/// Represents a session with the blog database.

View File

@ -1,14 +1,14 @@
using System.ComponentModel.DataAnnotations.Schema;
using SmartFormat;
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <inheritdoc />
internal sealed class BlogPost : IBlogPost
{
/// <inheritdoc />
[NotMapped]
public IBlogAuthor Author { get; internal set; } = null!;
public IAuthor Author { get; internal set; } = null!;
/// <inheritdoc />
public string Body { get; set; } = string.Empty;

View File

@ -1,14 +1,14 @@
using System.ComponentModel.DataAnnotations.Schema;
using SmartFormat;
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <inheritdoc />
internal sealed class BlogPostDraft : IBlogPostDraft
{
/// <inheritdoc />
[NotMapped]
public IBlogAuthor Author { get; internal set; } = null!;
public IAuthor Author { get; internal set; } = null!;
/// <inheritdoc />
public string Body { get; set; } = string.Empty;

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <summary>
/// An enumeration of the possible visibilities of a blog post.

View File

@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace OliverBooth.Data.Blog.Configuration;
namespace OliverBooth.Common.Data.Blog.Configuration;
internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
{

View File

@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace OliverBooth.Data.Blog.Configuration;
namespace OliverBooth.Common.Data.Blog.Configuration;
internal sealed class BlogPostDraftConfiguration : IEntityTypeConfiguration<BlogPostDraft>
{

View File

@ -1,9 +1,9 @@
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <summary>
/// Represents the author of a blog post.
/// </summary>
public interface IBlogAuthor
public interface IAuthor
{
/// <summary>
/// Gets the URL of the author's avatar.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <summary>
/// Represents a blog post.
@ -9,7 +9,7 @@ public interface IBlogPost
/// Gets the author of the post.
/// </summary>
/// <value>The author of the post.</value>
IBlogAuthor Author { get; }
IAuthor Author { get; }
/// <summary>
/// Gets or sets the body of the post.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Blog;
namespace OliverBooth.Common.Data.Blog;
/// <summary>
/// Represents a draft of a blog post.
@ -9,7 +9,7 @@ public interface IBlogPostDraft
/// Gets the author of the post.
/// </summary>
/// <value>The author of the post.</value>
IBlogAuthor Author { get; }
IAuthor Author { get; }
/// <summary>
/// Gets or sets the body of the post.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data;
namespace OliverBooth.Common.Data;
/// <summary>
/// Represents a permission.

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace OliverBooth.Data;
namespace OliverBooth.Common.Data.ValueConverters;
internal sealed class PermissionListConverter : ValueConverter<IReadOnlyList<Permission>, string>
{

View File

@ -3,7 +3,7 @@ using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Processing;
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Books;
/// <summary>
/// Represents a book.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Books;
/// <summary>
/// Represents the state of a book.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Books;
/// <summary>
/// Represents a book.

View File

@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OliverBooth.Common.Data.Web.Contact;
namespace OliverBooth.Data.Web.Configuration;
namespace OliverBooth.Common.Data.Web.Configuration;
/// <summary>
/// Represents the configuration for the <see cref="BlacklistEntry" /> entity.

View File

@ -1,8 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OliverBooth.Common.Data.Web.Books;
namespace OliverBooth.Data.Web.Configuration;
namespace OliverBooth.Common.Data.Web.Configuration;
/// <summary>
/// Represents the configuration for the <see cref="Book" /> entity.

View File

@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OliverBooth.Common.Data.Web.Projects;
namespace OliverBooth.Data.Web.Configuration;
namespace OliverBooth.Common.Data.Web.Configuration;
/// <summary>
/// Represents the configuration for the <see cref="ProgrammingLanguage" /> entity.

View File

@ -1,8 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OliverBooth.Common.Data.Web.Projects;
namespace OliverBooth.Data.Web.Configuration;
namespace OliverBooth.Common.Data.Web.Configuration;
/// <summary>
/// Represents the configuration for the <see cref="Project" /> entity.

View File

@ -1,8 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OliverBooth.Common.Data.Web.Users;
namespace OliverBooth.Data.Web.Configuration;
namespace OliverBooth.Common.Data.Web.Configuration;
internal sealed class SessionConfiguration : IEntityTypeConfiguration<Session>
{

View File

@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace OliverBooth.Data.Web.Configuration;
namespace OliverBooth.Common.Data.Web.Configuration;
/// <summary>
/// Represents the configuration for the <see cref="SiteConfiguration" /> entity.

View File

@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace OliverBooth.Data.Web.Configuration;
namespace OliverBooth.Common.Data.Web.Configuration;
/// <summary>
/// Represents the configuration for the <see cref="Template" /> entity.

View File

@ -1,7 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OliverBooth.Common.Data.ValueConverters;
using OliverBooth.Common.Data.Web.Users;
namespace OliverBooth.Data.Web.Configuration;
namespace OliverBooth.Common.Data.Web.Configuration;
internal sealed class UserConfiguration : IEntityTypeConfiguration<User>
{

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Contact;
/// <inheritdoc cref="IBlacklistEntry"/>
internal sealed class BlacklistEntry : IEquatable<BlacklistEntry>, IBlacklistEntry

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Contact;
/// <summary>
/// Represents an entry in the blacklist.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a template.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Projects;
/// <summary>
/// Represents a programming language.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Projects;
/// <summary>
/// Represents a project.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Projects;
/// <inheritdoc cref="IProgrammingLanguage" />
internal sealed class ProgrammingLanguage : IEquatable<ProgrammingLanguage>, IProgrammingLanguage

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Projects;
/// <summary>
/// Represents a project.

View File

@ -1,6 +1,6 @@
using System.ComponentModel;
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Projects;
/// <summary>
/// Represents the status of a project.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a site configuration item.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a MediaWiki-style template.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Users;
/// <summary>
/// Represents a temporary token used to correlate MFA attempts with the user.
@ -34,4 +34,4 @@ public interface IMfaToken
/// </summary>
/// <value>The user.</value>
IUser User { get; }
}
}

View File

@ -1,6 +1,6 @@
using System.Net;
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Users;
/// <summary>
/// Represents a login session.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Users;
/// <summary>
/// Represents a user which can log in to the blog.

View File

@ -1,6 +1,6 @@
using OliverBooth.Services;
using OliverBooth.Common.Services;
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Users;
/// <summary>
/// An enumeration of possible results for <see cref="IUserService.VerifyMfaRequest" />.

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Users;
internal sealed class MfaToken : IMfaToken
{

View File

@ -1,6 +1,6 @@
using System.Net;
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Users;
internal sealed class Session : ISession
{

View File

@ -2,15 +2,15 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Cryptography;
using System.Text;
using Cysharp.Text;
using OliverBooth.Data.Blog;
using OliverBooth.Common.Data.Blog;
using OtpNet;
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web.Users;
/// <summary>
/// Represents a user.
/// </summary>
internal sealed class User : IUser, IBlogAuthor
internal sealed class User : IUser, IAuthor
{
/// <inheritdoc cref="IUser.AvatarUrl" />
[NotMapped]

View File

@ -1,7 +1,12 @@
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Web.Configuration;
using Microsoft.Extensions.Configuration;
using OliverBooth.Common.Data.Web.Books;
using OliverBooth.Common.Data.Web.Configuration;
using OliverBooth.Common.Data.Web.Contact;
using OliverBooth.Common.Data.Web.Projects;
using OliverBooth.Common.Data.Web.Users;
namespace OliverBooth.Data.Web;
namespace OliverBooth.Common.Data.Web;
/// <summary>
/// Represents a session with the web database.

View File

@ -0,0 +1,43 @@
using Markdig;
using Microsoft.Extensions.DependencyInjection;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Markdown.Template;
using OliverBooth.Common.Services;
using X10D.Hosting.DependencyInjection;
namespace OliverBooth.Common.Extensions;
/// <summary>
/// Extension methods for dependency injection.
/// </summary>
public static class DependencyInjectionExtensions
{
/// <summary>
/// Adds all required services provided by the assembly to the current <see cref="IServiceCollection" />.
/// </summary>
/// <param name="collection">The <see cref="IServiceCollection" /> to add the service to.</param>
public static void AddCommonServices(this IServiceCollection collection)
{
collection.AddSingleton(provider => new MarkdownPipelineBuilder()
// .Use<TimestampExtension>()
.Use(new TemplateExtension(provider.GetRequiredService<ITemplateService>()))
.UseAdvancedExtensions()
.UseBootstrap()
.UseEmojiAndSmiley()
.UseSmartyPants()
.Build());
collection.AddDbContextFactory<BlogContext>();
collection.AddDbContextFactory<WebContext>();
collection.AddSingleton<IBlogPostService, BlogPostService>();
collection.AddSingleton<IContactService, ContactService>();
collection.AddSingleton<IProjectService, ProjectService>();
collection.AddSingleton<IReadingListService, ReadingListService>();
collection.AddSingleton<ITemplateService, TemplateService>();
collection.AddHostedSingleton<ISessionService, SessionService>();
collection.AddHostedSingleton<IUserService, UserService>();
}
}

View File

@ -1,6 +1,7 @@
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Hosting;
namespace OliverBooth.Extensions;
namespace OliverBooth.Common.Extensions;
/// <summary>
/// Extension methods for <see cref="IWebHostBuilder" />.

View File

@ -1,7 +1,7 @@
using System.Globalization;
using SmartFormat.Core.Extensions;
namespace OliverBooth.Formatting;
namespace OliverBooth.Common.Formatting;
/// <summary>
/// Represents a SmartFormat formatter that formats a date.

View File

@ -1,7 +1,8 @@
using Markdig;
using Microsoft.Extensions.DependencyInjection;
using SmartFormat.Core.Extensions;
namespace OliverBooth.Formatting;
namespace OliverBooth.Common.Formatting;
/// <summary>
/// Represents a SmartFormat formatter that formats markdown.

View File

@ -1,8 +1,8 @@
using Markdig;
using Markdig.Renderers;
using OliverBooth.Services;
using OliverBooth.Common.Services;
namespace OliverBooth.Markdown.Template;
namespace OliverBooth.Common.Markdown.Template;
/// <summary>
/// Represents a Markdown extension that adds support for MediaWiki-style templates.

View File

@ -1,6 +1,6 @@
using Markdig.Syntax.Inlines;
namespace OliverBooth.Markdown.Template;
namespace OliverBooth.Common.Markdown.Template;
/// <summary>
/// Represents a Markdown inline element that represents a MediaWiki-style template.

View File

@ -2,7 +2,7 @@ using Cysharp.Text;
using Markdig.Helpers;
using Markdig.Parsers;
namespace OliverBooth.Markdown.Template;
namespace OliverBooth.Common.Markdown.Template;
/// <summary>
/// Represents a Markdown inline parser that handles MediaWiki-style templates.
@ -17,7 +17,7 @@ public sealed class TemplateInlineParser : InlineParser
/// </summary>
public TemplateInlineParser()
{
OpeningCharacters = new[] { '{' };
OpeningCharacters = ['{'];
}
/// <inheritdoc />

View File

@ -1,8 +1,8 @@
using Markdig.Renderers;
using Markdig.Renderers.Html;
using OliverBooth.Services;
using OliverBooth.Common.Services;
namespace OliverBooth.Markdown.Template;
namespace OliverBooth.Common.Markdown.Template;
/// <summary>
/// Represents a Markdown object renderer that handles <see cref="TemplateInline" /> elements.

View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Alexinea.Extensions.Configuration.Toml" Version="7.0.0"/>
<PackageReference Include="Asp.Versioning.Mvc" Version="8.0.0"/>
<PackageReference Include="BCrypt.Net-Core" Version="1.6.0"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="Markdig" Version="0.35.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0"/>
<PackageReference Include="NetBarcode" Version="1.7.0"/>
<PackageReference Include="Otp.NET" Version="1.3.0"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0"/>
<PackageReference Include="Serilog" Version="3.1.1"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/>
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
<PackageReference Include="SmartFormat.NET" Version="3.3.2"/>
<PackageReference Include="X10D" Version="3.3.1"/>
<PackageReference Include="X10D.Hosting" Version="3.3.1"/>
<PackageReference Include="ZString" Version="2.5.1"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Markdown\"/>
</ItemGroup>
</Project>

View File

@ -2,16 +2,28 @@ using System.Diagnostics.CodeAnalysis;
using Humanizer;
using Markdig;
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Blog;
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Data.Web.Users;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents an implementation of <see cref="IBlogPostService" />.
/// </summary>
internal sealed class BlogPostService : IBlogPostService
{
/*private static readonly JsonSerializerOptions EditorJsOptions = new()
{
ReferenceHandler = ReferenceHandler.Preserve,
Converters =
{
new ParagraphBlockConverter(),
new HeadingBlockConverter(),
new MarkdownDocumentConverter()
}
};
*/
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
private readonly IUserService _userService;
private readonly MarkdownPipeline _markdownPipeline;
@ -33,6 +45,19 @@ internal sealed class BlogPostService : IBlogPostService
_markdownPipeline = markdownPipeline;
}
/// <inheritdoc />
public string GetBlogPostEditorObject(IBlogPost post)
{
if (post is null)
{
throw new ArgumentNullException(nameof(post));
}
/*var document = (JsonDocument)Markdig.Markdown.Convert(post.Body, new JsonRenderer(), _markdownPipeline);
return JsonSerializer.Serialize(document, EditorJsOptions);*/
return """{"blocks":{}}""";
}
/// <inheritdoc />
public int GetBlogPostCount()
{
@ -196,7 +221,7 @@ internal sealed class BlogPostService : IBlogPostService
return post;
}
if (_userService.TryGetUser(post.AuthorId, out IUser? user) && user is IBlogAuthor author)
if (_userService.TryGetUser(post.AuthorId, out IUser? user) && user is IAuthor author)
{
post.Author = author;
}

View File

@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Data.Web.Contact;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <inheritdoc cref="IContactService" />
internal sealed class ContactService : IContactService

View File

@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Blog;
using OliverBooth.Common.Data.Blog;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service for managing blog posts.
@ -27,6 +27,14 @@ public interface IBlogPostService
/// <returns>The total number of blog posts.</returns>
int GetBlogPostCount();
/// <summary>
/// Returns a JSON object representing the blog post block data.
/// </summary>
/// <param name="post">The blog post whose block data object should be returned.</param>
/// <returns>The JSON data of the blog post block data.</returns>
/// <exception cref="ArgumentNullException"><paramref name="post" /> is <see langword="null" />.</exception>
string GetBlogPostEditorObject(IBlogPost post);
/// <summary>
/// Returns a collection of blog posts from the specified page, optionally limiting the number of posts
/// returned per page.
@ -34,7 +42,7 @@ public interface IBlogPostService
/// <param name="page">The zero-based index of the page to return.</param>
/// <param name="pageSize">The maximum number of posts to return per page.</param>
/// <returns>A collection of blog posts.</returns>
IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize = 10);
IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize);
/// <summary>
/// Returns the drafts of this post, sorted by their update timestamp.

View File

@ -1,6 +1,6 @@
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web.Contact;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service for managing contact information.

View File

@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web.Projects;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service for interacting with projects.

View File

@ -1,6 +1,6 @@
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web.Books;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service which fetches books from the reading list.

View File

@ -1,9 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Web;
using ISession = OliverBooth.Data.Web.ISession;
using OliverBooth.Common.Data.Web.Users;
using ISession = OliverBooth.Common.Data.Web.Users.ISession;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
public interface ISessionService
{

View File

@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Web;
using OliverBooth.Markdown.Template;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Markdown.Template;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service that renders MediaWiki-style templates.

View File

@ -1,10 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web.Users;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service for managing users.
/// Represents a service which manages users.
/// </summary>
public interface IUserService
{
@ -34,15 +34,16 @@ public interface IUserService
void DeleteToken(string token);
/// <summary>
/// Attempts to find a user with the specified ID.
/// Attempts to find a user by their unique ID.
/// </summary>
/// <param name="id">The ID of the user to find.</param>
/// <param name="id">The ID of the user to return.</param>
/// <param name="user">
/// When this method returns, contains the user with the specified ID, if the user is found; otherwise,
/// <see langword="null" />.
/// When this method returns, contains the user whose ID is equal to the specified <paramref name="id" />, if
/// such a user exists; otherwise, <see langword="null" />.
/// </param>
/// <returns>
/// <see langword="true" /> if a user with the specified ID is found; otherwise, <see langword="false" />.
/// <see langword="true" /> if a user was found with the specified <paramref name="id" />; otherwise,
/// <see langword="false" />.
/// </returns>
bool TryGetUser(Guid id, [NotNullWhen(true)] out IUser? user);

View File

@ -2,9 +2,10 @@ using System.Diagnostics.CodeAnalysis;
using Humanizer;
using Markdig;
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Data.Web.Projects;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service for interacting with projects.

View File

@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Web;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Data.Web.Books;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
internal sealed class ReadingListService : IReadingListService
{

View File

@ -1,13 +1,16 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using OliverBooth.Data.Blog;
using OliverBooth.Data.Web;
using ISession = OliverBooth.Data.Web.ISession;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Data.Web.Users;
using ISession = OliverBooth.Common.Data.Web.Users.ISession;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
internal sealed class SessionService : BackgroundService, ISessionService
{
@ -20,11 +23,9 @@ internal sealed class SessionService : BackgroundService, ISessionService
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="userService">The user service.</param>
/// <param name="blogContextFactory">The <see cref="BlogContext" /> factory.</param>
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
public SessionService(ILogger<SessionService> logger,
IUserService userService,
IDbContextFactory<BlogContext> blogContextFactory,
IDbContextFactory<WebContext> webContextFactory)
{
_logger = logger;

View File

@ -1,13 +1,13 @@
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Web;
using OliverBooth.Formatting;
using OliverBooth.Markdown.Template;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Formatting;
using OliverBooth.Common.Markdown.Template;
using SmartFormat;
using SmartFormat.Extensions;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents a service that renders MediaWiki-style templates.
@ -22,7 +22,7 @@ internal sealed class TemplateService : ITemplateService
/// Initializes a new instance of the <see cref="TemplateService" /> class.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
/// <param name="webContextFactory">The <see cref="Data.Web.WebContext" /> factory.</param>
public TemplateService(IServiceProvider serviceProvider,
IDbContextFactory<WebContext> webContextFactory)
{

View File

@ -2,15 +2,14 @@ using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Web;
using Microsoft.Extensions.Hosting;
using OliverBooth.Common.Data.Web;
using OliverBooth.Common.Data.Web.Users;
using BC = BCrypt.Net.BCrypt;
using Timer = System.Timers.Timer;
namespace OliverBooth.Services;
namespace OliverBooth.Common.Services;
/// <summary>
/// Represents an implementation of <see cref="IUserService" />.
/// </summary>
internal sealed class UserService : BackgroundService, IUserService
{
private static readonly RandomNumberGenerator RandomNumberGenerator = RandomNumberGenerator.Create();
@ -117,19 +116,8 @@ internal sealed class UserService : BackgroundService, IUserService
/// <inheritdoc />
public bool TryGetUser(Guid id, [NotNullWhen(true)] out IUser? user)
{
if (_userCache.TryGetValue(id, out user))
{
return true;
}
using WebContext context = _dbContextFactory.CreateDbContext();
user = context.Users.Find(id);
if (user is not null)
{
_userCache.TryAdd(id, user);
}
user = context.Users.FirstOrDefault(u => u.Id == id);
return user is not null;
}
@ -138,7 +126,12 @@ internal sealed class UserService : BackgroundService, IUserService
{
using WebContext context = _dbContextFactory.CreateDbContext();
user = context.Users.FirstOrDefault(u => u.EmailAddress == email);
return user is not null && BC.Verify(password, ((User)user).Password);
if (user is not null && !BC.Verify(password, ((User)user).Password))
{
user = null;
}
return user is not null;
}
/// <inheritdoc />

View File

@ -13,6 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Gulpfile.mjs = Gulpfile.mjs
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Api", "OliverBooth.Api\OliverBooth.Api.csproj", "{37423EB9-C025-45C5-B3B9-B09610FC4829}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Common", "OliverBooth.Common\OliverBooth.Common.csproj", "{77DC9941-E648-442F-935A-C66FC401ECBC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -23,6 +27,14 @@ Global
{A58A6FA3-480C-400B-822A-3786741BF39C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.Build.0 = Release|Any CPU
{37423EB9-C025-45C5-B3B9-B09610FC4829}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{37423EB9-C025-45C5-B3B9-B09610FC4829}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37423EB9-C025-45C5-B3B9-B09610FC4829}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37423EB9-C025-45C5-B3B9-B09610FC4829}.Release|Any CPU.Build.0 = Release|Any CPU
{77DC9941-E648-442F-935A-C66FC401ECBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77DC9941-E648-442F-935A-C66FC401ECBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77DC9941-E648-442F-935A-C66FC401ECBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77DC9941-E648-442F-935A-C66FC401ECBC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection

View File

@ -1,9 +1,9 @@
using System.Diagnostics;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using ISession = OliverBooth.Data.Web.ISession;
using OliverBooth.Common.Data.Web.Users;
using OliverBooth.Common.Services;
using ISession = OliverBooth.Common.Data.Web.Users.ISession;
namespace OliverBooth.Controllers.Api.v1;

View File

@ -1,8 +1,8 @@
using System.Xml.Serialization;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Blog;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
using OliverBooth.Data.Blog.Rss;
using OliverBooth.Services;
namespace OliverBooth.Controllers.Blog;

View File

@ -1,7 +1,7 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Web.Contact;
using OliverBooth.Common.Services;
namespace OliverBooth.Controllers;

View File

@ -13,7 +13,7 @@ public sealed class TimestampInlineParser : InlineParser
/// </summary>
public TimestampInlineParser()
{
OpeningCharacters = new[] { '<' };
OpeningCharacters = ['<'];
}
/// <inheritdoc />

View File

@ -27,32 +27,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Alexinea.Extensions.Configuration.Toml" Version="7.0.0"/>
<PackageReference Include="Asp.Versioning.Mvc" Version="8.0.0"/>
<PackageReference Include="AspNetCore.ReCaptcha" Version="1.8.1"/>
<PackageReference Include="BCrypt.Net-Core" Version="1.6.0"/>
<PackageReference Include="FluentFTP" Version="49.0.2" />
<PackageReference Include="FluentFTP.Logging" Version="1.0.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.59"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="MailKit" Version="4.3.0"/>
<PackageReference Include="MailKitSimplified.Sender" Version="2.9.0"/>
<PackageReference Include="Markdig" Version="0.35.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.2"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.2"/>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.2"/>
<PackageReference Include="NetBarcode" Version="1.7.0"/>
<PackageReference Include="Otp.NET" Version="1.3.0"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0"/>
<PackageReference Include="Serilog" Version="3.1.1"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/>
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
<PackageReference Include="SmartFormat.NET" Version="3.3.2"/>
<PackageReference Include="X10D" Version="3.3.1"/>
<PackageReference Include="X10D.Hosting" Version="3.3.1"/>
<PackageReference Include="ZString" Version="2.5.1"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OliverBooth.Common\OliverBooth.Common.csproj"/>
</ItemGroup>
</Project>

View File

@ -1,9 +1,10 @@
@page "/admin/blog-posts"
@using System.Diagnostics
@using OliverBooth.Data
@using OliverBooth.Data.Blog
@using OliverBooth.Data.Web
@using OliverBooth.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using OliverBooth.Common.Data
@using OliverBooth.Common.Data.Blog
@using OliverBooth.Common.Data.Web.Users
@using OliverBooth.Common.Services
@model OliverBooth.Pages.Admin.BlogPosts
@inject IBlogPostService BlogPostService
@inject IUserService UserService
@ -52,7 +53,7 @@
<th>Options</th>
</tr>
</thead>
<tbody>
@foreach (IBlogPost post in BlogPostService.GetAllBlogPosts(visibility: (BlogPostVisibility)(-1)))
{

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Web.Users;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Admin;

View File

@ -1,6 +1,7 @@
@page "/admin/blog-posts/edit/{id}"
@using Markdig
@using OliverBooth.Data.Blog
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using OliverBooth.Common.Data.Blog
@model OliverBooth.Pages.Admin.EditBlogPost
@inject MarkdownPipeline MarkdownPipeline

View File

@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Blog;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Data.Web.Users;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Admin;

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Web.Users;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Admin;

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Services;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Admin;

View File

@ -1,5 +1,5 @@
@page "/admin/login/mfa"
@using OliverBooth.Data.Web
@using OliverBooth.Common.Data.Web.Users
@model OliverBooth.Pages.Admin.MultiFactorStep
@{

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Services;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Admin;

View File

@ -1,7 +1,7 @@
@page "/blog/{year:int}/{month:int}/{day:int}/{slug}"
@using Humanizer
@using OliverBooth.Data.Blog
@using OliverBooth.Services
@using OliverBooth.Common.Data.Blog
@using OliverBooth.Common.Services
@inject IBlogPostService BlogPostService
@model Article
@ -29,7 +29,7 @@
@{
ViewData["Post"] = post;
ViewData["Title"] = post.Title;
IBlogAuthor author = post.Author;
IAuthor author = post.Author;
DateTimeOffset published = post.Published;
}

View File

@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Primitives;
using OliverBooth.Data.Blog;
using OliverBooth.Services;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
using BC = BCrypt.Net.BCrypt;
namespace OliverBooth.Pages.Blog;

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Blog;
using OliverBooth.Services;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Blog;

View File

@ -1,8 +1,8 @@
using Cysharp.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Blog;
using OliverBooth.Services;
using OliverBooth.Common.Data.Blog;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Blog;
@ -31,7 +31,7 @@ public class RawArticle : PageModel
return NotFound();
}
Response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
Response.Headers.Append("Content-Type", "text/plain; charset=utf-8");
using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder();
builder.AppendLine("# " + post.Title);

View File

@ -1,5 +1,5 @@
@page
@using OliverBooth.Data.Web
@using OliverBooth.Common.Data.Web.Books
@model OliverBooth.Pages.Books
@{
ViewData["Title"] = "Reading List";

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Web.Books;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages;

View File

@ -1,5 +1,5 @@
@using OliverBooth.Services
@using OliverBooth.Data.Blog
@using OliverBooth.Common.Data.Blog
@using OliverBooth.Common.Services
@implements IDisposable
@inject IBlogPostService BlogPostService
@inject IJSRuntime JsRuntime

View File

@ -1,6 +1,6 @@
@page
@using OliverBooth.Data.Web
@using OliverBooth.Services
@using OliverBooth.Common.Data.Web.Contact
@using OliverBooth.Common.Services
@inject IContactService ContactService
@{
ViewData["Title"] = "Blacklist";

View File

@ -1,6 +1,6 @@
@page
@using OliverBooth.Data.Web
@using OliverBooth.Services
@using OliverBooth.Common.Data.Web.Projects
@using OliverBooth.Common.Services
@inject IProjectService ProjectService
@{
ViewData["Title"] = "Projects";

View File

@ -1,7 +1,7 @@
@page "/project/{slug}"
@using Markdig
@using OliverBooth.Data.Web
@using OliverBooth.Services
@using OliverBooth.Common.Data.Web.Projects
@using OliverBooth.Common.Services
@model Project
@inject IProjectService ProjectService
@inject MarkdownPipeline MarkdownPipeline

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Web;
using OliverBooth.Services;
using OliverBooth.Common.Data.Web.Projects;
using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Projects;

View File

@ -1,6 +1,6 @@
@using OliverBooth.Data.Blog
@using OliverBooth.Data.Web
@using OliverBooth.Services
@using OliverBooth.Common.Data.Web.Users
@using OliverBooth.Common.Services
@using OliverBooth.Common.Data.Blog
@inject IBlogPostService BlogPostService
@inject IUserService UserService
@inject ISessionService SessionService

View File

@ -1,6 +1,6 @@
@using OliverBooth.Data.Blog
@using OliverBooth.Data.Web
@using OliverBooth.Services
@using OliverBooth.Common.Data.Web.Users
@using OliverBooth.Common.Services
@using OliverBooth.Common.Data.Blog
@inject IBlogPostService BlogPostService
@inject IUserService UserService
@inject ISessionService SessionService

Some files were not shown because too many files have changed in this diff Show More