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 Asp.Versioning;
using Humanizer; using Humanizer;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Blog; using OliverBooth.Api.Data;
using OliverBooth.Data.Web; using OliverBooth.Common.Data.Blog;
using OliverBooth.Services; 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] [ApiController]
[Route("api/v{version:apiVersion}/blog")] [Route("blog")]
[Produces("application/json")] [Produces("application/json")]
[ApiVersion(1)] [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 IBlogPostService _blogPostService;
private readonly IUserService _userService; private readonly IUserService _userService;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BlogApiController" /> class. /// Initializes a new instance of the <see cref="BlogController" /> class.
/// </summary> /// </summary>
/// <param name="blogPostService">The <see cref="IBlogPostService" />.</param> /// <param name="blogPostService">The <see cref="IBlogPostService" />.</param>
/// <param name="userService">The <see cref="IUserService" />.</param> /// <param name="userService">The <see cref="IUserService" />.</param>
public BlogApiController(IBlogPostService blogPostService, IUserService userService) public BlogController(IBlogPostService blogPostService, IUserService userService)
{ {
_blogPostService = blogPostService; _blogPostService = blogPostService;
_userService = userService; _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() public IActionResult Count()
{ {
return Ok(new { count = _blogPostService.GetBlogPostCount() }); 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?}")] [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) public IActionResult GetAllBlogPosts(int page = 0)
{ {
const int itemsPerPage = 10; const int itemsPerPage = 10;
@ -44,7 +56,15 @@ public sealed class BlogApiController : ControllerBase
return Ok(allPosts.Select(post => CreatePostObject(post))); 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?}")] [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) public IActionResult GetTaggedBlogPosts(string tag, int page = 0)
{ {
const int itemsPerPage = 10; const int itemsPerPage = 10;
@ -55,13 +75,18 @@ public sealed class BlogApiController : ControllerBase
return Ok(allPosts.Select(post => CreatePostObject(post))); 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}")] [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) public IActionResult GetAuthor(Guid id)
{ {
if (!_userService.TryGetUser(id, out IUser? author)) if (!_userService.TryGetUser(id, out IUser? author)) return NotFound();
{
return NotFound();
}
return Ok(new 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?}")] [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) public IActionResult GetPost(Guid id)
{ {
if (!_blogPostService.TryGetPost(id, out IBlogPost? post)) if (!_blogPostService.TryGetPost(id, out IBlogPost? post)) return NotFound();
{
return NotFound();
}
return Ok(CreatePostObject(post, true)); 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 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> /// <summary>
/// Represents a session with the blog database. /// Represents a session with the blog database.

View File

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

View File

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

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Blog; namespace OliverBooth.Common.Data.Blog;
/// <summary> /// <summary>
/// An enumeration of the possible visibilities of a blog post. /// 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.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace OliverBooth.Data.Blog.Configuration; namespace OliverBooth.Common.Data.Blog.Configuration;
internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost> internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace OliverBooth.Data; namespace OliverBooth.Common.Data.ValueConverters;
internal sealed class PermissionListConverter : ValueConverter<IReadOnlyList<Permission>, string> 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.Formats.Png;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
namespace OliverBooth.Data.Web; namespace OliverBooth.Common.Data.Web.Books;
/// <summary> /// <summary>
/// Represents a book. /// Represents a book.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 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> internal sealed class SessionConfiguration : IEntityTypeConfiguration<Session>
{ {

View File

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

View File

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

View File

@ -1,7 +1,9 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; 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> 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"/> /// <inheritdoc cref="IBlacklistEntry"/>
internal sealed class BlacklistEntry : IEquatable<BlacklistEntry>, 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> /// <summary>
/// Represents an entry in the blacklist. /// Represents an entry in the blacklist.

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web; namespace OliverBooth.Common.Data.Web.Projects;
/// <inheritdoc cref="IProgrammingLanguage" /> /// <inheritdoc cref="IProgrammingLanguage" />
internal sealed class ProgrammingLanguage : IEquatable<ProgrammingLanguage>, 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> /// <summary>
/// Represents a project. /// Represents a project.

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web; namespace OliverBooth.Common.Data.Web.Users;
/// <summary> /// <summary>
/// Represents a temporary token used to correlate MFA attempts with the user. /// Represents a temporary token used to correlate MFA attempts with the user.

View File

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

View File

@ -1,4 +1,4 @@
namespace OliverBooth.Data.Web; namespace OliverBooth.Common.Data.Web.Users;
/// <summary> /// <summary>
/// Represents a user which can log in to the blog. /// 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> /// <summary>
/// An enumeration of possible results for <see cref="IUserService.VerifyMfaRequest" />. /// 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 internal sealed class MfaToken : IMfaToken
{ {

View File

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

View File

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

View File

@ -1,7 +1,12 @@
using Microsoft.EntityFrameworkCore; 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> /// <summary>
/// Represents a session with the web database. /// 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 System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Hosting;
namespace OliverBooth.Extensions; namespace OliverBooth.Common.Extensions;
/// <summary> /// <summary>
/// Extension methods for <see cref="IWebHostBuilder" />. /// Extension methods for <see cref="IWebHostBuilder" />.

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
using Markdig.Syntax.Inlines; using Markdig.Syntax.Inlines;
namespace OliverBooth.Markdown.Template; namespace OliverBooth.Common.Markdown.Template;
/// <summary> /// <summary>
/// Represents a Markdown inline element that represents a MediaWiki-style template. /// 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.Helpers;
using Markdig.Parsers; using Markdig.Parsers;
namespace OliverBooth.Markdown.Template; namespace OliverBooth.Common.Markdown.Template;
/// <summary> /// <summary>
/// Represents a Markdown inline parser that handles MediaWiki-style templates. /// Represents a Markdown inline parser that handles MediaWiki-style templates.
@ -17,7 +17,7 @@ public sealed class TemplateInlineParser : InlineParser
/// </summary> /// </summary>
public TemplateInlineParser() public TemplateInlineParser()
{ {
OpeningCharacters = new[] { '{' }; OpeningCharacters = ['{'];
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -1,8 +1,8 @@
using Markdig.Renderers; using Markdig.Renderers;
using Markdig.Renderers.Html; using Markdig.Renderers.Html;
using OliverBooth.Services; using OliverBooth.Common.Services;
namespace OliverBooth.Markdown.Template; namespace OliverBooth.Common.Markdown.Template;
/// <summary> /// <summary>
/// Represents a Markdown object renderer that handles <see cref="TemplateInline" /> elements. /// 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 Humanizer;
using Markdig; using Markdig;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Blog; using OliverBooth.Common.Data.Blog;
using OliverBooth.Data.Web; using OliverBooth.Common.Data.Web.Users;
namespace OliverBooth.Services; namespace OliverBooth.Common.Services;
/// <summary> /// <summary>
/// Represents an implementation of <see cref="IBlogPostService" />. /// Represents an implementation of <see cref="IBlogPostService" />.
/// </summary> /// </summary>
internal sealed class BlogPostService : IBlogPostService 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 IDbContextFactory<BlogContext> _dbContextFactory;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly MarkdownPipeline _markdownPipeline; private readonly MarkdownPipeline _markdownPipeline;
@ -33,6 +45,19 @@ internal sealed class BlogPostService : IBlogPostService
_markdownPipeline = markdownPipeline; _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 /> /// <inheritdoc />
public int GetBlogPostCount() public int GetBlogPostCount()
{ {
@ -196,7 +221,7 @@ internal sealed class BlogPostService : IBlogPostService
return post; 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; post.Author = author;
} }

View File

@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore; 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" /> /// <inheritdoc cref="IContactService" />
internal sealed class ContactService : IContactService internal sealed class ContactService : IContactService

View File

@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Blog; using OliverBooth.Common.Data.Blog;
namespace OliverBooth.Services; namespace OliverBooth.Common.Services;
/// <summary> /// <summary>
/// Represents a service for managing blog posts. /// Represents a service for managing blog posts.
@ -27,6 +27,14 @@ public interface IBlogPostService
/// <returns>The total number of blog posts.</returns> /// <returns>The total number of blog posts.</returns>
int GetBlogPostCount(); 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> /// <summary>
/// Returns a collection of blog posts from the specified page, optionally limiting the number of posts /// Returns a collection of blog posts from the specified page, optionally limiting the number of posts
/// returned per page. /// 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="page">The zero-based index of the page to return.</param>
/// <param name="pageSize">The maximum number of posts to return per page.</param> /// <param name="pageSize">The maximum number of posts to return per page.</param>
/// <returns>A collection of blog posts.</returns> /// <returns>A collection of blog posts.</returns>
IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize = 10); IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize);
/// <summary> /// <summary>
/// Returns the drafts of this post, sorted by their update timestamp. /// 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> /// <summary>
/// Represents a service for managing contact information. /// Represents a service for managing contact information.

View File

@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using OliverBooth.Data.Web; using OliverBooth.Common.Data.Web.Projects;
namespace OliverBooth.Services; namespace OliverBooth.Common.Services;
/// <summary> /// <summary>
/// Represents a service for interacting with projects. /// 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> /// <summary>
/// Represents a service which fetches books from the reading list. /// Represents a service which fetches books from the reading list.

View File

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

View File

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

View File

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

View File

@ -2,9 +2,10 @@ using System.Diagnostics.CodeAnalysis;
using Humanizer; using Humanizer;
using Markdig; using Markdig;
using Microsoft.EntityFrameworkCore; 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> /// <summary>
/// Represents a service for interacting with projects. /// Represents a service for interacting with projects.

View File

@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore; 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 internal sealed class ReadingListService : IReadingListService
{ {

View File

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

View File

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

View File

@ -2,15 +2,14 @@ using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography; using System.Security.Cryptography;
using Microsoft.EntityFrameworkCore; 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 BC = BCrypt.Net.BCrypt;
using Timer = System.Timers.Timer; 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 internal sealed class UserService : BackgroundService, IUserService
{ {
private static readonly RandomNumberGenerator RandomNumberGenerator = RandomNumberGenerator.Create(); private static readonly RandomNumberGenerator RandomNumberGenerator = RandomNumberGenerator.Create();
@ -117,19 +116,8 @@ internal sealed class UserService : BackgroundService, IUserService
/// <inheritdoc /> /// <inheritdoc />
public bool TryGetUser(Guid id, [NotNullWhen(true)] out IUser? user) public bool TryGetUser(Guid id, [NotNullWhen(true)] out IUser? user)
{ {
if (_userCache.TryGetValue(id, out user))
{
return true;
}
using WebContext context = _dbContextFactory.CreateDbContext(); using WebContext context = _dbContextFactory.CreateDbContext();
user = context.Users.Find(id); user = context.Users.FirstOrDefault(u => u.Id == id);
if (user is not null)
{
_userCache.TryAdd(id, user);
}
return user is not null; return user is not null;
} }
@ -138,7 +126,12 @@ internal sealed class UserService : BackgroundService, IUserService
{ {
using WebContext context = _dbContextFactory.CreateDbContext(); using WebContext context = _dbContextFactory.CreateDbContext();
user = context.Users.FirstOrDefault(u => u.EmailAddress == email); 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 /> /// <inheritdoc />

View File

@ -13,6 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Gulpfile.mjs = Gulpfile.mjs Gulpfile.mjs = Gulpfile.mjs
EndProjectSection EndProjectSection
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
EndGlobalSection EndGlobalSection

View File

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

View File

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

View File

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

View File

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

View File

@ -27,32 +27,17 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="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="HtmlAgilityPack" Version="1.11.59"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="MailKit" Version="4.3.0"/> <PackageReference Include="MailKit" Version="4.3.0"/>
<PackageReference Include="MailKitSimplified.Sender" Version="2.9.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.Components.Web" Version="8.0.2"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" 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="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.2"/>
<PackageReference Include="NetBarcode" Version="1.7.0"/> </ItemGroup>
<PackageReference Include="Otp.NET" Version="1.3.0"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0"/> <ItemGroup>
<PackageReference Include="Serilog" Version="3.1.1"/> <ProjectReference Include="..\OliverBooth.Common\OliverBooth.Common.csproj"/>
<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>
</Project> </Project>

View File

@ -1,9 +1,10 @@
@page "/admin/blog-posts" @page "/admin/blog-posts"
@using System.Diagnostics @using System.Diagnostics
@using OliverBooth.Data @using Microsoft.AspNetCore.Mvc.TagHelpers
@using OliverBooth.Data.Blog @using OliverBooth.Common.Data
@using OliverBooth.Data.Web @using OliverBooth.Common.Data.Blog
@using OliverBooth.Services @using OliverBooth.Common.Data.Web.Users
@using OliverBooth.Common.Services
@model OliverBooth.Pages.Admin.BlogPosts @model OliverBooth.Pages.Admin.BlogPosts
@inject IBlogPostService BlogPostService @inject IBlogPostService BlogPostService
@inject IUserService UserService @inject IUserService UserService

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
using Cysharp.Text; using Cysharp.Text;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Blog; using OliverBooth.Common.Data.Blog;
using OliverBooth.Services; using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Blog; namespace OliverBooth.Pages.Blog;
@ -31,7 +31,7 @@ public class RawArticle : PageModel
return NotFound(); 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(); using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder();
builder.AppendLine("# " + post.Title); builder.AppendLine("# " + post.Title);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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