diff --git a/Deployment/mssql-migration.sql b/Deployment/mssql-migration.sql index b931a83dd..1aa9aec1d 100644 --- a/Deployment/mssql-migration.sql +++ b/Deployment/mssql-migration.sql @@ -1 +1,33 @@ --- \ No newline at end of file +-- v14.3.x - v14.4.0 +CREATE TABLE [dbo].[LoginHistory]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [LoginTimeUtc] [datetime] NOT NULL, + [LoginIp] [nvarchar](64) NULL, + [LoginUserAgent] [nvarchar](128) NULL, + [DeviceFingerprint] [nvarchar](128) NULL, + CONSTRAINT [PK_LoginHistory] PRIMARY KEY CLUSTERED +( + [Id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] +) ON [PRIMARY] +GO + +DROP TABLE [LocalAccount] +GO + +EXEC sys.sp_rename + @objname = N'Category.RouteName', + @newname = 'Slug', + @objtype = 'COLUMN' +GO + +IF EXISTS ( + SELECT 1 + FROM sys.columns c + JOIN sys.objects o ON c.object_id = o.object_id + WHERE o.name = 'Post' AND c.name = 'InlineCss' +) +BEGIN + ALTER TABLE Post DROP COLUMN InlineCss; +END; +GO \ No newline at end of file diff --git a/README.md b/README.md index 1489281c2..49b48648e 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ Open Search | Search | Supported | `/opensearch` Pingback | Social | Supported | `/pingback` Reader View | Reader mode | Supported | N/A FOAF | Social | Supported | `/foaf.xml` -RSD | Service Discovery | Supported | `/rsd` *If MetaWeblog is enabled* -MetaWeblog | Blogging | Basic Support | `/metaweblog` +RSD | Service Discovery | Deprecated | N/A +MetaWeblog | Blogging | Deprecated | N/A Dublin Core Metadata | SEO | Basic Support | N/A BlogML | Blogging | Not planned | APML | Social | Not planned | diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ac92d6bf3..a14e0a905 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,8 +3,8 @@ Edi Wang edi.wang (C) 2024 edi.wang@outlook.com - 14.3.3.0 - 14.3.3.0 - 14.3.3 + 14.4.0.0 + 14.4.0.0 + 14.4.0 \ No newline at end of file diff --git a/src/Moonglade.Auth/Account.cs b/src/Moonglade.Auth/Account.cs deleted file mode 100644 index 9d4f95601..000000000 --- a/src/Moonglade.Auth/Account.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Moonglade.Data.Entities; - -namespace Moonglade.Auth; - -public class Account -{ - public Guid Id { get; set; } - public string Username { get; set; } - public DateTime? LastLoginTimeUtc { get; set; } - public string LastLoginIp { get; set; } - public DateTime CreateTimeUtc { get; set; } - - public Account() - { - - } - - public Account(LocalAccountEntity entity) - { - if (null == entity) return; - - Id = entity.Id; - CreateTimeUtc = entity.CreateTimeUtc; - LastLoginIp = entity.LastLoginIp.Trim(); - LastLoginTimeUtc = entity.LastLoginTimeUtc.GetValueOrDefault(); - Username = entity.Username.Trim(); - } -} \ No newline at end of file diff --git a/src/Moonglade.Auth/AccountExistsQuery.cs b/src/Moonglade.Auth/AccountExistsQuery.cs deleted file mode 100644 index a0ce6aaf6..000000000 --- a/src/Moonglade.Auth/AccountExistsQuery.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Auth; - -public record AccountExistsQuery(string Username) : IRequest; - -public class AccountExistsQueryHandler(IRepository repo) : IRequestHandler -{ - public Task Handle(AccountExistsQuery request, CancellationToken ct) => - repo.AnyAsync(p => p.Username == request.Username.ToLower(), ct); -} \ No newline at end of file diff --git a/src/Moonglade.Auth/ChangePasswordCommand.cs b/src/Moonglade.Auth/ChangePasswordCommand.cs deleted file mode 100644 index 7e1d384cc..000000000 --- a/src/Moonglade.Auth/ChangePasswordCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; -using Moonglade.Utils; - -namespace Moonglade.Auth; - -public record ChangePasswordCommand(Guid Id, string ClearPassword) : IRequest; - -public class ChangePasswordCommandHandler(IRepository repo) : IRequestHandler -{ - public async Task Handle(ChangePasswordCommand request, CancellationToken ct) - { - var account = await repo.GetAsync(request.Id, ct); - if (account is null) - { - throw new InvalidOperationException($"LocalAccountEntity with Id '{request.Id}' not found."); - } - - var newSalt = Helper.GenerateSalt(); - account.PasswordSalt = newSalt; - account.PasswordHash = Helper.HashPassword2(request.ClearPassword, newSalt); - - await repo.UpdateAsync(account, ct); - } -} \ No newline at end of file diff --git a/src/Moonglade.Auth/CountAccountsQuery.cs b/src/Moonglade.Auth/CountAccountsQuery.cs deleted file mode 100644 index a61d336cb..000000000 --- a/src/Moonglade.Auth/CountAccountsQuery.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Auth; - -public record CountAccountsQuery : IRequest; - -public class CountAccountsQueryHandler(IRepository repo) : IRequestHandler -{ - public Task Handle(CountAccountsQuery request, CancellationToken ct) => repo.CountAsync(ct: ct); -} \ No newline at end of file diff --git a/src/Moonglade.Auth/CreateAccountCommand.cs b/src/Moonglade.Auth/CreateAccountCommand.cs deleted file mode 100644 index 9f71d2619..000000000 --- a/src/Moonglade.Auth/CreateAccountCommand.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; -using Moonglade.Utils; -using System.ComponentModel.DataAnnotations; - -namespace Moonglade.Auth; - -public class CreateAccountCommand : IRequest -{ - [Required] - [Display(Name = "Username")] - [MinLength(2), MaxLength(32)] - [RegularExpression("[a-z0-9]+")] - public string Username { get; set; } - - [Required] - [Display(Name = "Password")] - [MinLength(8), MaxLength(32)] - [DataType(DataType.Password)] - [RegularExpression(@"^(?=.*[a-zA-Z])(?=.*[0-9])[A-Za-z0-9._~!@#$^&*]{8,}$")] - public string Password { get; set; } -} - -public class CreateAccountCommandHandler(IRepository repo) : IRequestHandler -{ - public Task Handle(CreateAccountCommand request, CancellationToken ct) - { - if (string.IsNullOrWhiteSpace(request.Username)) - { - throw new ArgumentNullException(nameof(request.Username), "value must not be empty."); - } - - if (string.IsNullOrWhiteSpace(request.Password)) - { - throw new ArgumentNullException(nameof(request.Password), "value must not be empty."); - } - - var uid = Guid.NewGuid(); - var salt = Helper.GenerateSalt(); - var hash = Helper.HashPassword2(request.Password.Trim(), salt); - - var account = new LocalAccountEntity - { - Id = uid, - CreateTimeUtc = DateTime.UtcNow, - Username = request.Username.ToLower().Trim(), - PasswordSalt = salt, - PasswordHash = hash - }; - - return repo.AddAsync(account, ct); - } -} \ No newline at end of file diff --git a/src/Moonglade.Auth/DeleteAccountCommand.cs b/src/Moonglade.Auth/DeleteAccountCommand.cs deleted file mode 100644 index a8dd6a888..000000000 --- a/src/Moonglade.Auth/DeleteAccountCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Auth; - -public record DeleteAccountCommand(Guid Id) : IRequest; - -public class DeleteAccountCommandHandler(IRepository repo) : IRequestHandler -{ - public async Task Handle(DeleteAccountCommand request, CancellationToken ct) - { - var account = await repo.GetAsync(request.Id, ct); - if (account != null) await repo.DeleteAsync(request.Id, ct); - } -} \ No newline at end of file diff --git a/src/Moonglade.Auth/GetAccountQuery.cs b/src/Moonglade.Auth/GetAccountQuery.cs deleted file mode 100644 index ae83c1bae..000000000 --- a/src/Moonglade.Auth/GetAccountQuery.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Auth; - -public record GetAccountQuery(Guid Id) : IRequest; - -public class GetAccountQueryHandler(IRepository repo) : IRequestHandler -{ - public async Task Handle(GetAccountQuery request, CancellationToken ct) - { - var entity = await repo.GetAsync(request.Id, ct); - var item = new Account(entity); - return item; - } -} \ No newline at end of file diff --git a/src/Moonglade.Auth/GetAccountsQuery.cs b/src/Moonglade.Auth/GetAccountsQuery.cs deleted file mode 100644 index 4ac6961e6..000000000 --- a/src/Moonglade.Auth/GetAccountsQuery.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Auth; - -public record GetAccountsQuery : IRequest>; - -public class GetAccountsQueryHandler(IRepository repo) : IRequestHandler> -{ - public Task> Handle(GetAccountsQuery request, CancellationToken ct) - { - return repo.SelectAsync(p => new Account - { - Id = p.Id, - CreateTimeUtc = p.CreateTimeUtc, - LastLoginIp = p.LastLoginIp, - LastLoginTimeUtc = p.LastLoginTimeUtc, - Username = p.Username - }, ct); - } -} \ No newline at end of file diff --git a/src/Moonglade.Auth/GetLoginHistoryQuery.cs b/src/Moonglade.Auth/GetLoginHistoryQuery.cs new file mode 100644 index 000000000..3fe788932 --- /dev/null +++ b/src/Moonglade.Auth/GetLoginHistoryQuery.cs @@ -0,0 +1,16 @@ +using Moonglade.Data; +using Moonglade.Data.Entities; +using Moonglade.Data.Specifications; + +namespace Moonglade.Auth; + +public record GetLoginHistoryQuery : IRequest>; + +public class GetLoginHistoryQueryHandler(MoongladeRepository repo) : IRequestHandler> +{ + public async Task> Handle(GetLoginHistoryQuery request, CancellationToken ct) + { + var history = await repo.ListAsync(new LoginHistorySpec(10), ct); + return history; + } +} \ No newline at end of file diff --git a/src/Moonglade.Auth/LogSuccessLoginCommand.cs b/src/Moonglade.Auth/LogSuccessLoginCommand.cs index 463cef937..51146d081 100644 --- a/src/Moonglade.Auth/LogSuccessLoginCommand.cs +++ b/src/Moonglade.Auth/LogSuccessLoginCommand.cs @@ -1,22 +1,22 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; +using Moonglade.Data; +using Moonglade.Data.Entities; namespace Moonglade.Auth; -public record LogSuccessLoginCommand(Guid Id, string IpAddress) : IRequest; +public record LogSuccessLoginCommand(string IpAddress, string UserAgent, string DeviceFingerprint) : IRequest; -public class LogSuccessLoginCommandHandler(IRepository repo) : IRequestHandler +public class LogSuccessLoginCommandHandler(MoongladeRepository repo) : IRequestHandler { public async Task Handle(LogSuccessLoginCommand request, CancellationToken ct) { - var (id, ipAddress) = request; - - var entity = await repo.GetAsync(id, ct); - if (entity is not null) + var entity = new LoginHistoryEntity { - entity.LastLoginIp = ipAddress.Trim(); - entity.LastLoginTimeUtc = DateTime.UtcNow; - await repo.UpdateAsync(entity, ct); - } + LoginIp = request.IpAddress.Trim(), + LoginTimeUtc = DateTime.UtcNow, + LoginUserAgent = request.UserAgent.Trim(), + DeviceFingerprint = request.DeviceFingerprint.Trim() + }; + + await repo.AddAsync(entity, ct); } } \ No newline at end of file diff --git a/src/Moonglade.Auth/Moonglade.Auth.csproj b/src/Moonglade.Auth/Moonglade.Auth.csproj index a8f928369..e89187834 100644 --- a/src/Moonglade.Auth/Moonglade.Auth.csproj +++ b/src/Moonglade.Auth/Moonglade.Auth.csproj @@ -16,9 +16,10 @@ - + + diff --git a/src/Moonglade.Auth/UpdateLocalAccountPasswordRequest.cs b/src/Moonglade.Auth/UpdateLocalAccountPasswordRequest.cs new file mode 100644 index 000000000..736a712ab --- /dev/null +++ b/src/Moonglade.Auth/UpdateLocalAccountPasswordRequest.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Moonglade.Auth; + +public class UpdateLocalAccountPasswordRequest +{ + [Required] + [RegularExpression("^[A-Za-z0-9]{3,16}$")] + public string NewUsername { get; set; } + + [Required] + [RegularExpression("^(?=.*[a-zA-Z])(?=.*[0-9])[A-Za-z0-9._~!@#$^&*]{8,}$")] + public string OldPassword { get; set; } + + [Required] + [RegularExpression("^(?=.*[a-zA-Z])(?=.*[0-9])[A-Za-z0-9._~!@#$^&*]{8,}$")] + public string NewPassword { get; set; } +} \ No newline at end of file diff --git a/src/Moonglade.Auth/ValidateLoginCommand.cs b/src/Moonglade.Auth/ValidateLoginCommand.cs index 8e2464994..19317237c 100644 --- a/src/Moonglade.Auth/ValidateLoginCommand.cs +++ b/src/Moonglade.Auth/ValidateLoginCommand.cs @@ -1,34 +1,21 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; +using Moonglade.Configuration; using Moonglade.Utils; namespace Moonglade.Auth; -public record ValidateLoginCommand(string Username, string InputPassword) : IRequest; +public record ValidateLoginCommand(string Username, string InputPassword) : IRequest; -public class ValidateLoginCommandHandler(IRepository repo) : IRequestHandler +public class ValidateLoginCommandHandler(IBlogConfig config) : IRequestHandler { - public async Task Handle(ValidateLoginCommand request, CancellationToken ct) + public Task Handle(ValidateLoginCommand request, CancellationToken ct) { - var account = await repo.GetAsync(p => p.Username == request.Username); - if (account is null) return Guid.Empty; + var account = config.LocalAccountSettings; - var valid = account.PasswordHash == (string.IsNullOrWhiteSpace(account.PasswordSalt) - ? Helper.HashPassword(request.InputPassword.Trim()) - : Helper.HashPassword2(request.InputPassword.Trim(), account.PasswordSalt)); + if (account is null) return Task.FromResult(false); + if (account.Username != request.Username) return Task.FromResult(false); - // migrate old account to salt - if (valid && string.IsNullOrWhiteSpace(account.PasswordSalt)) - { - var salt = Helper.GenerateSalt(); - var newHash = Helper.HashPassword2(request.InputPassword.Trim(), salt); + var valid = account.PasswordHash == Helper.HashPassword(request.InputPassword.Trim(), account.PasswordSalt); - account.PasswordSalt = salt; - account.PasswordHash = newHash; - - await repo.UpdateAsync(account, ct); - } - - return valid ? account.Id : Guid.Empty; + return Task.FromResult(valid); } } \ No newline at end of file diff --git a/src/Moonglade.Comments/Comment.cs b/src/Moonglade.Comments/Comment.cs index feaf008c5..06ef738f2 100644 --- a/src/Moonglade.Comments/Comment.cs +++ b/src/Moonglade.Comments/Comment.cs @@ -13,7 +13,7 @@ public class Comment public string CommentContent { get; set; } - public IReadOnlyList CommentReplies { get; set; } + public List CommentReplies { get; set; } } public class CommentDetailedItem : Comment diff --git a/src/Moonglade.Comments/CountCommentsQuery.cs b/src/Moonglade.Comments/CountCommentsQuery.cs index 3b37ec06d..b18fbffdc 100644 --- a/src/Moonglade.Comments/CountCommentsQuery.cs +++ b/src/Moonglade.Comments/CountCommentsQuery.cs @@ -1,12 +1,12 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; namespace Moonglade.Comments; public record CountCommentsQuery : IRequest; -public class CountCommentsQueryHandler(IRepository repo) : IRequestHandler +public class CountCommentsQueryHandler(MoongladeRepository repo) : IRequestHandler { - public Task Handle(CountCommentsQuery request, CancellationToken ct) => repo.CountAsync(ct: ct); + public Task Handle(CountCommentsQuery request, CancellationToken ct) => repo.CountAsync(ct); } \ No newline at end of file diff --git a/src/Moonglade.Comments/CreateCommentCommand.cs b/src/Moonglade.Comments/CreateCommentCommand.cs index d5d82b0b7..62dd13341 100644 --- a/src/Moonglade.Comments/CreateCommentCommand.cs +++ b/src/Moonglade.Comments/CreateCommentCommand.cs @@ -1,13 +1,13 @@ using MediatR; -using Moonglade.Comments.Moderator; +using Microsoft.Extensions.Logging; using Moonglade.Configuration; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; -using Moonglade.Data.Spec; +using Moonglade.Data.Specifications; namespace Moonglade.Comments; -public class CreateCommentCommand(Guid postId, CommentRequest payload, string ipAddress) : IRequest<(int Status, CommentDetailedItem Item)> +public class CreateCommentCommand(Guid postId, CommentRequest payload, string ipAddress) : IRequest { public Guid PostId { get; set; } = postId; @@ -16,43 +16,21 @@ public class CreateCommentCommand(Guid postId, CommentRequest payload, string ip public string IpAddress { get; set; } = ipAddress; } -public class CreateCommentCommandHandler(IBlogConfig blogConfig, IRepository postRepo, IModeratorService moderator, IRepository commentRepo) : - IRequestHandler +public class CreateCommentCommandHandler( + IBlogConfig blogConfig, + ILogger logger, + MoongladeRepository postRepo, + MoongladeRepository commentRepo) : IRequestHandler { - public async Task<(int Status, CommentDetailedItem Item)> Handle(CreateCommentCommand request, CancellationToken ct) + public async Task Handle(CreateCommentCommand request, CancellationToken ct) { - if (blogConfig.ContentSettings.EnableWordFilter) - { - switch (blogConfig.ContentSettings.WordFilterMode) - { - case WordFilterMode.Mask: - request.Payload.Username = await moderator.Mask(request.Payload.Username); - request.Payload.Content = await moderator.Mask(request.Payload.Content); - break; - case WordFilterMode.Block: - if (await moderator.Detect(request.Payload.Username, request.Payload.Content)) - { - await Task.CompletedTask; - return (-1, null); - } - break; - } - } - - var spec = new PostSpec(request.PostId, false); - var postInfo = await postRepo.FirstOrDefaultAsync(spec, p => new - { - p.Title, - p.PubDateUtc - }); + var spec = new PostByIdForTitleDateSpec(request.PostId); + var postInfo = await postRepo.FirstOrDefaultAsync(spec, ct); if (blogConfig.ContentSettings.CloseCommentAfterDays > 0) { var days = DateTime.UtcNow.Date.Subtract(postInfo.PubDateUtc.GetValueOrDefault()).Days; - if (days > blogConfig.ContentSettings.CloseCommentAfterDays) - { - return (-2, null); - } + if (days > blogConfig.ContentSettings.CloseCommentAfterDays) return null; } var model = new CommentEntity @@ -81,6 +59,7 @@ public class CreateCommentCommandHandler(IBlogConfig blogConfig, IRepository commentRepo, IRepository commentReplyRepo) : IRequestHandler +public class DeleteCommentsCommandHandler( + MoongladeRepository commentRepo, + ILogger logger) : IRequestHandler { public async Task Handle(DeleteCommentsCommand request, CancellationToken ct) { - var spec = new CommentSpec(request.Ids); - var comments = await commentRepo.ListAsync(spec); + var spec = new CommentByIdsSepc(request.Ids); + var comments = await commentRepo.ListAsync(spec, ct); foreach (var cmt in comments) { - // 1. Delete all replies - var cReplies = await commentReplyRepo.ListAsync(new CommentReplySpec(cmt.Id)); - if (cReplies.Any()) - { - await commentReplyRepo.DeleteAsync(cReplies, ct); - } - - // 2. Delete comment itself + cmt.Replies.Clear(); await commentRepo.DeleteAsync(cmt, ct); } + + logger.LogInformation("Deleted {Count} comment(s)", comments.Count); } } \ No newline at end of file diff --git a/src/Moonglade.Comments/GetApprovedCommentsQuery.cs b/src/Moonglade.Comments/GetApprovedCommentsQuery.cs index 80c7b2f58..a4678ca31 100644 --- a/src/Moonglade.Comments/GetApprovedCommentsQuery.cs +++ b/src/Moonglade.Comments/GetApprovedCommentsQuery.cs @@ -1,17 +1,17 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; -using Moonglade.Data.Spec; +using Moonglade.Data.Specifications; namespace Moonglade.Comments; -public record GetApprovedCommentsQuery(Guid PostId) : IRequest>; +public record GetApprovedCommentsQuery(Guid PostId) : IRequest>; -public class GetApprovedCommentsQueryHandler(IRepository repo) : IRequestHandler> +public class GetApprovedCommentsQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(GetApprovedCommentsQuery request, CancellationToken ct) + public Task> Handle(GetApprovedCommentsQuery request, CancellationToken ct) { - return repo.SelectAsync(new CommentSpec(request.PostId), c => new Comment + return repo.SelectAsync(new CommentWithRepliesSpec(request.PostId), c => new Comment { CommentContent = c.CommentContent, CreateTimeUtc = c.CreateTimeUtc, diff --git a/src/Moonglade.Comments/GetCommentsQuery.cs b/src/Moonglade.Comments/GetCommentsQuery.cs index 1b191ed0a..ee3c3ce19 100644 --- a/src/Moonglade.Comments/GetCommentsQuery.cs +++ b/src/Moonglade.Comments/GetCommentsQuery.cs @@ -1,17 +1,17 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; -using Moonglade.Data.Spec; +using Moonglade.Data.Specifications; namespace Moonglade.Comments; -public record GetCommentsQuery(int PageSize, int PageIndex) : IRequest>; +public record GetCommentsQuery(int PageSize, int PageIndex) : IRequest>; -public class GetCommentsQueryHandler(IRepository repo) : IRequestHandler> +public class GetCommentsQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(GetCommentsQuery request, CancellationToken ct) + public Task> Handle(GetCommentsQuery request, CancellationToken ct) { - var spec = new CommentSpec(request.PageSize, request.PageIndex); + var spec = new CommentPagingSepc(request.PageSize, request.PageIndex); var comments = repo.SelectAsync(spec, CommentDetailedItem.EntitySelector, ct); return comments; diff --git a/src/Moonglade.Comments/Moderator/ModeratorService.cs b/src/Moonglade.Comments/Moderator/ModeratorService.cs index d6555fb10..095714cd3 100644 --- a/src/Moonglade.Comments/Moderator/ModeratorService.cs +++ b/src/Moonglade.Comments/Moderator/ModeratorService.cs @@ -54,14 +54,14 @@ public async Task Mask(string input) var payload = new Payload { OriginAspNetRequestId = _httpContextAccessor.HttpContext?.TraceIdentifier, - Contents = new[] - { + Contents = + [ new Content { Id = "0", RawText = input } - } + ] }; var response = await _httpClient.PostAsync( diff --git a/src/Moonglade.Comments/ReplyCommentCommand.cs b/src/Moonglade.Comments/ReplyCommentCommand.cs index f13717ada..21ca456f8 100644 --- a/src/Moonglade.Comments/ReplyCommentCommand.cs +++ b/src/Moonglade.Comments/ReplyCommentCommand.cs @@ -1,17 +1,21 @@ using MediatR; +using Microsoft.Extensions.Logging; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; using Moonglade.Utils; namespace Moonglade.Comments; public record ReplyCommentCommand(Guid CommentId, string ReplyContent) : IRequest; -public class ReplyCommentCommandHandler(IRepository commentRepo, IRepository commentReplyRepo) : IRequestHandler +public class ReplyCommentCommandHandler( + ILogger logger, + MoongladeRepository commentRepo, + MoongladeRepository commentReplyRepo) : IRequestHandler { public async Task Handle(ReplyCommentCommand request, CancellationToken ct) { - var cmt = await commentRepo.GetAsync(request.CommentId, ct); + var cmt = await commentRepo.GetByIdAsync(request.CommentId, ct); if (cmt is null) throw new InvalidOperationException($"Comment {request.CommentId} is not found."); var id = Guid.NewGuid(); @@ -40,6 +44,7 @@ public async Task Handle(ReplyCommentCommand request, Cancellation Title = cmt.Post.Title }; + logger.LogInformation("Replied comment '{CommentId}' with reply '{ReplyId}'", request.CommentId, id); return reply; } } \ No newline at end of file diff --git a/src/Moonglade.Comments/ToggleApprovalCommand.cs b/src/Moonglade.Comments/ToggleApprovalCommand.cs index 1f408e004..3400046b2 100644 --- a/src/Moonglade.Comments/ToggleApprovalCommand.cs +++ b/src/Moonglade.Comments/ToggleApprovalCommand.cs @@ -1,22 +1,27 @@ using MediatR; +using Microsoft.Extensions.Logging; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; -using Moonglade.Data.Spec; +using Moonglade.Data.Specifications; namespace Moonglade.Comments; public record ToggleApprovalCommand(Guid[] CommentIds) : IRequest; -public class ToggleApprovalCommandHandler(IRepository repo) : IRequestHandler +public class ToggleApprovalCommandHandler( + MoongladeRepository repo, + ILogger logger) : IRequestHandler { public async Task Handle(ToggleApprovalCommand request, CancellationToken ct) { - var spec = new CommentSpec(request.CommentIds); - var comments = await repo.ListAsync(spec); + var spec = new CommentByIdsSepc(request.CommentIds); + var comments = await repo.ListAsync(spec, ct); foreach (var cmt in comments) { cmt.IsApproved = !cmt.IsApproved; await repo.UpdateAsync(cmt, ct); } + + logger.LogInformation("Toggled approval status for {Count} comment(s)", comments.Count); } } \ No newline at end of file diff --git a/src/Moonglade.Configuration/AddDefaultConfigurationCommand.cs b/src/Moonglade.Configuration/AddDefaultConfigurationCommand.cs index 13ba5b3d2..497e34e51 100644 --- a/src/Moonglade.Configuration/AddDefaultConfigurationCommand.cs +++ b/src/Moonglade.Configuration/AddDefaultConfigurationCommand.cs @@ -1,13 +1,12 @@ using MediatR; using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; namespace Moonglade.Configuration; public record AddDefaultConfigurationCommand(int Id, string CfgKey, string DefaultJson) : IRequest; -public class AddDefaultConfigurationCommandHandler(IRepository repository) : IRequestHandler +public class AddDefaultConfigurationCommandHandler(MoongladeRepository repository) : IRequestHandler { public async Task Handle(AddDefaultConfigurationCommand request, CancellationToken ct) { diff --git a/src/Moonglade.Configuration/AdvancedSettings.cs b/src/Moonglade.Configuration/AdvancedSettings.cs index cf76b97b6..6338b24d2 100644 --- a/src/Moonglade.Configuration/AdvancedSettings.cs +++ b/src/Moonglade.Configuration/AdvancedSettings.cs @@ -18,9 +18,6 @@ public class AdvancedSettings : IBlogSettings [Display(Name = "Enable Pingback")] public bool EnablePingback { get; set; } = true; - [Display(Name = "Enable MetaWeblog API")] - public bool EnableMetaWeblog { get; set; } = true; - [Display(Name = "Enable OpenSearch")] public bool EnableOpenSearch { get; set; } = true; @@ -33,15 +30,9 @@ public class AdvancedSettings : IBlogSettings [Display(Name = "Enable Site Map")] public bool EnableSiteMap { get; set; } = true; - [MinLength(8), MaxLength(16)] - [Display(Name = "MetaWeblog password")] - public string MetaWeblogPassword { get; set; } - [Display(Name = "Show warning when clicking external links")] public bool WarnExternalLink { get; set; } - public string MetaWeblogPasswordHash { get; set; } - [JsonIgnore] public static AdvancedSettings DefaultValue => new(); } \ No newline at end of file diff --git a/src/Moonglade.Configuration/BlogConfig.cs b/src/Moonglade.Configuration/BlogConfig.cs index 96a1bcda1..d9660d06e 100644 --- a/src/Moonglade.Configuration/BlogConfig.cs +++ b/src/Moonglade.Configuration/BlogConfig.cs @@ -15,6 +15,7 @@ public interface IBlogConfig AdvancedSettings AdvancedSettings { get; set; } CustomStyleSheetSettings CustomStyleSheetSettings { get; set; } CustomMenuSettings CustomMenuSettings { get; set; } + LocalAccountSettings LocalAccountSettings { get; set; } IEnumerable LoadFromConfig(IDictionary config); KeyValuePair UpdateAsync(T blogSettings) where T : IBlogSettings; @@ -38,6 +39,8 @@ public class BlogConfig : IBlogConfig public CustomMenuSettings CustomMenuSettings { get; set; } + public LocalAccountSettings LocalAccountSettings { get; set; } + public IEnumerable LoadFromConfig(IDictionary config) { ContentSettings = AssignValueForConfigItem(1, ContentSettings.DefaultValue, config); @@ -48,11 +51,12 @@ public IEnumerable LoadFromConfig(IDictionary config) AdvancedSettings = AssignValueForConfigItem(6, AdvancedSettings.DefaultValue, config); CustomStyleSheetSettings = AssignValueForConfigItem(7, CustomStyleSheetSettings.DefaultValue, config); CustomMenuSettings = AssignValueForConfigItem(10, CustomMenuSettings.DefaultValue, config); + LocalAccountSettings = AssignValueForConfigItem(11, LocalAccountSettings.DefaultValue, config); return _keysToInit.AsEnumerable(); } - private readonly List _keysToInit = new(); + private readonly List _keysToInit = []; private T AssignValueForConfigItem(int index, T defaultValue, IDictionary config) where T : IBlogSettings { var name = typeof(T).Name; diff --git a/src/Moonglade.Configuration/CustomMenuSettings.cs b/src/Moonglade.Configuration/CustomMenuSettings.cs index 093e2acbd..57c848aa2 100644 --- a/src/Moonglade.Configuration/CustomMenuSettings.cs +++ b/src/Moonglade.Configuration/CustomMenuSettings.cs @@ -20,26 +20,21 @@ public class CustomMenuSettings : IBlogSettings public Menu[] Menus { get; set; } = Array.Empty(); [JsonIgnore] - public static CustomMenuSettings DefaultValue - { - get + public static CustomMenuSettings DefaultValue => + new() { - return new() - { - IsEnabled = true, - Menus = new[] + IsEnabled = true, + Menus = + [ + new Menu { - new Menu - { - Title = "About", - Url = "/page/about", - Icon = "bi-star", - DisplayOrder = 1, - IsOpenInNewTab = false, - SubMenus = new() - } + Title = "About", + Url = "/page/about", + Icon = "bi-star", + DisplayOrder = 1, + IsOpenInNewTab = false, + SubMenus = [] } - }; - } - } + ] + }; } \ No newline at end of file diff --git a/src/Moonglade.Configuration/FeedSettings.cs b/src/Moonglade.Configuration/FeedSettings.cs index 504da6c1d..c702055c3 100644 --- a/src/Moonglade.Configuration/FeedSettings.cs +++ b/src/Moonglade.Configuration/FeedSettings.cs @@ -5,8 +5,8 @@ namespace Moonglade.Configuration; public class FeedSettings : IBlogSettings { - [Display(Name = "RSS items")] - public int RssItemCount { get; set; } + [Display(Name = "Feed items")] + public int FeedItemCount { get; set; } [Display(Name = "Use full blog post content instead of abstract")] public bool UseFullContent { get; set; } @@ -14,7 +14,7 @@ public class FeedSettings : IBlogSettings [JsonIgnore] public static FeedSettings DefaultValue => new() { - RssItemCount = 20, + FeedItemCount = 20, UseFullContent = false }; } \ No newline at end of file diff --git a/src/Moonglade.Configuration/GetAllConfigurationsQuery.cs b/src/Moonglade.Configuration/GetAllConfigurationsQuery.cs index 13ca8abcd..728c1b378 100644 --- a/src/Moonglade.Configuration/GetAllConfigurationsQuery.cs +++ b/src/Moonglade.Configuration/GetAllConfigurationsQuery.cs @@ -1,16 +1,16 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; namespace Moonglade.Configuration; public record GetAllConfigurationsQuery : IRequest>; -public class GetAllConfigurationsQueryHandler(IRepository repo) : IRequestHandler> +public class GetAllConfigurationsQueryHandler(MoongladeRepository repo) : IRequestHandler> { public async Task> Handle(GetAllConfigurationsQuery request, CancellationToken ct) { - var entities = await repo.SelectAsync(p => new { p.CfgKey, p.CfgValue }, ct); + var entities = await repo.ListAsync(ct); return entities.ToDictionary(k => k.CfgKey, v => v.CfgValue); } } \ No newline at end of file diff --git a/src/Moonglade.Configuration/LocalAccountSettings.cs b/src/Moonglade.Configuration/LocalAccountSettings.cs new file mode 100644 index 000000000..e122f9229 --- /dev/null +++ b/src/Moonglade.Configuration/LocalAccountSettings.cs @@ -0,0 +1,17 @@ +namespace Moonglade.Configuration; + +public class LocalAccountSettings : IBlogSettings +{ + public string Username { get; set; } + + public string PasswordHash { get; set; } + public string PasswordSalt { get; set; } + + public static LocalAccountSettings DefaultValue => + new() + { + Username = "admin", + PasswordHash = "bXHAa7tEsZmCh1pYcHPotNlP0gaYfzIkxKuHoJnHMt0=", + PasswordSalt = "Hq8jxngFtTEtl3UI294K7w==" + }; +} \ No newline at end of file diff --git a/src/Moonglade.Configuration/Menu.cs b/src/Moonglade.Configuration/Menu.cs index 84c1f281d..0117774b9 100644 --- a/src/Moonglade.Configuration/Menu.cs +++ b/src/Moonglade.Configuration/Menu.cs @@ -12,5 +12,5 @@ public class Menu public bool IsOpenInNewTab { get; set; } - public List SubMenus { get; set; } = new(); + public List SubMenus { get; set; } = []; } \ No newline at end of file diff --git a/src/Moonglade.Configuration/UpdateConfigurationCommand.cs b/src/Moonglade.Configuration/UpdateConfigurationCommand.cs index 7b4119c49..0e6721e1c 100644 --- a/src/Moonglade.Configuration/UpdateConfigurationCommand.cs +++ b/src/Moonglade.Configuration/UpdateConfigurationCommand.cs @@ -1,18 +1,18 @@ using MediatR; using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; +using Moonglade.Data.Specifications; namespace Moonglade.Configuration; public record UpdateConfigurationCommand(string Name, string Json) : IRequest; -public class UpdateConfigurationCommandHandler(IRepository repository) : IRequestHandler +public class UpdateConfigurationCommandHandler(MoongladeRepository repository) : IRequestHandler { public async Task Handle(UpdateConfigurationCommand request, CancellationToken ct) { var (name, json) = request; - var entity = await repository.GetAsync(p => p.CfgKey == name); + var entity = await repository.FirstOrDefaultAsync(new BlogConfigurationSpec(name), ct); if (entity == null) return OperationCode.ObjectNotFound; entity.CfgValue = json; diff --git a/src/Moonglade.Core/CategoryFeature/Category.cs b/src/Moonglade.Core/CategoryFeature/Category.cs deleted file mode 100644 index c69bcecde..000000000 --- a/src/Moonglade.Core/CategoryFeature/Category.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Linq.Expressions; - -namespace Moonglade.Core.CategoryFeature; - -public class Category -{ - public Guid Id { get; set; } - public string RouteName { get; set; } - public string DisplayName { get; set; } - public string Note { get; set; } - - public static readonly Expression> EntitySelector = c => new() - { - Id = c.Id, - DisplayName = c.DisplayName, - RouteName = c.RouteName, - Note = c.Note - }; -} \ No newline at end of file diff --git a/src/Moonglade.Core/CategoryFeature/CreateCategoryCommand.cs b/src/Moonglade.Core/CategoryFeature/CreateCategoryCommand.cs index 85f111634..85da377d2 100644 --- a/src/Moonglade.Core/CategoryFeature/CreateCategoryCommand.cs +++ b/src/Moonglade.Core/CategoryFeature/CreateCategoryCommand.cs @@ -1,4 +1,7 @@ using Edi.CacheAside.InMemory; +using Microsoft.Extensions.Logging; +using Moonglade.Data; +using Moonglade.Data.Specifications; using System.ComponentModel.DataAnnotations; namespace Moonglade.Core.CategoryFeature; @@ -11,10 +14,10 @@ public class CreateCategoryCommand : IRequest public string DisplayName { get; set; } [Required] - [Display(Name = "Route Name")] + [Display(Name = "Slug")] [RegularExpression("(?!-)([a-z0-9-]+)")] [MaxLength(64)] - public string RouteName { get; set; } + public string Slug { get; set; } [Required] [Display(Name = "Description")] @@ -22,22 +25,27 @@ public class CreateCategoryCommand : IRequest public string Note { get; set; } } -public class CreateCategoryCommandHandler(IRepository catRepo, ICacheAside cache) : IRequestHandler +public class CreateCategoryCommandHandler( + MoongladeRepository catRepo, + ICacheAside cache, + ILogger logger) : IRequestHandler { public async Task Handle(CreateCategoryCommand request, CancellationToken ct) { - var exists = await catRepo.AnyAsync(c => c.RouteName == request.RouteName, ct); + var exists = await catRepo.AnyAsync(new CategoryBySlugSpec(request.Slug), ct); if (exists) return; var category = new CategoryEntity { Id = Guid.NewGuid(), - RouteName = request.RouteName.Trim(), + Slug = request.Slug.Trim(), Note = request.Note?.Trim(), DisplayName = request.DisplayName.Trim() }; await catRepo.AddAsync(category, ct); cache.Remove(BlogCachePartition.General.ToString(), "allcats"); + + logger.LogInformation("Category created: {Category}", category.Id); } } \ No newline at end of file diff --git a/src/Moonglade.Core/CategoryFeature/DeleteCategoryCommand.cs b/src/Moonglade.Core/CategoryFeature/DeleteCategoryCommand.cs index dad61fb10..fb687e82a 100644 --- a/src/Moonglade.Core/CategoryFeature/DeleteCategoryCommand.cs +++ b/src/Moonglade.Core/CategoryFeature/DeleteCategoryCommand.cs @@ -1,4 +1,5 @@ using Edi.CacheAside.InMemory; +using Microsoft.Extensions.Logging; using Moonglade.Data; namespace Moonglade.Core.CategoryFeature; @@ -6,21 +7,22 @@ namespace Moonglade.Core.CategoryFeature; public record DeleteCategoryCommand(Guid Id) : IRequest; public class DeleteCategoryCommandHandler( - IRepository catRepo, - IRepository postCatRepo, - ICacheAside cache) : IRequestHandler + MoongladeRepository catRepo, + ICacheAside cache, + ILogger logger) + : IRequestHandler { public async Task Handle(DeleteCategoryCommand request, CancellationToken ct) { - var exists = await catRepo.AnyAsync(c => c.Id == request.Id, ct); - if (!exists) return OperationCode.ObjectNotFound; + var cat = await catRepo.GetByIdAsync(request.Id, ct); + if (null == cat) return OperationCode.ObjectNotFound; - var pcs = await postCatRepo.GetAsync(pc => pc.CategoryId == request.Id); - if (pcs is not null) await postCatRepo.DeleteAsync(pcs, ct); + cat.PostCategory.Clear(); - await catRepo.DeleteAsync(request.Id, ct); + await catRepo.DeleteAsync(cat, ct); cache.Remove(BlogCachePartition.General.ToString(), "allcats"); + logger.LogInformation("Category deleted: {Category}", cat.Id); return OperationCode.Done; } } \ No newline at end of file diff --git a/src/Moonglade.Core/CategoryFeature/GetCategoriesQuery.cs b/src/Moonglade.Core/CategoryFeature/GetCategoriesQuery.cs index 7f16460f2..920360e9f 100644 --- a/src/Moonglade.Core/CategoryFeature/GetCategoriesQuery.cs +++ b/src/Moonglade.Core/CategoryFeature/GetCategoriesQuery.cs @@ -1,17 +1,18 @@ using Edi.CacheAside.InMemory; +using Moonglade.Data; namespace Moonglade.Core.CategoryFeature; -public record GetCategoriesQuery : IRequest>; +public record GetCategoriesQuery : IRequest>; -public class GetCategoriesQueryHandler(IRepository repo, ICacheAside cache) : IRequestHandler> +public class GetCategoriesQueryHandler(MoongladeRepository repo, ICacheAside cache) : IRequestHandler> { - public Task> Handle(GetCategoriesQuery request, CancellationToken ct) + public Task> Handle(GetCategoriesQuery request, CancellationToken ct) { return cache.GetOrCreateAsync(BlogCachePartition.General.ToString(), "allcats", async entry => { entry.SlidingExpiration = TimeSpan.FromHours(1); - var list = await repo.SelectAsync(Category.EntitySelector, ct); + var list = await repo.ListAsync(ct); return list; }); } diff --git a/src/Moonglade.Core/CategoryFeature/GetCategoryByRouteQuery.cs b/src/Moonglade.Core/CategoryFeature/GetCategoryByRouteQuery.cs deleted file mode 100644 index 731d01325..000000000 --- a/src/Moonglade.Core/CategoryFeature/GetCategoryByRouteQuery.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Moonglade.Data.Spec; - -namespace Moonglade.Core.CategoryFeature; - -public record GetCategoryByRouteQuery(string RouteName) : IRequest; - -public class GetCategoryByRouteQueryHandler(IRepository repo) : IRequestHandler -{ - public Task Handle(GetCategoryByRouteQuery request, CancellationToken ct) => - repo.FirstOrDefaultAsync(new CategorySpec(request.RouteName), Category.EntitySelector); -} \ No newline at end of file diff --git a/src/Moonglade.Core/CategoryFeature/GetCategoryBySlugQuery.cs b/src/Moonglade.Core/CategoryFeature/GetCategoryBySlugQuery.cs new file mode 100644 index 000000000..af65208e6 --- /dev/null +++ b/src/Moonglade.Core/CategoryFeature/GetCategoryBySlugQuery.cs @@ -0,0 +1,12 @@ +using Moonglade.Data; +using Moonglade.Data.Specifications; + +namespace Moonglade.Core.CategoryFeature; + +public record GetCategoryBySlugQuery(string Slug) : IRequest; + +public class GetCategoryByRouteQueryHandler(MoongladeRepository repo) : IRequestHandler +{ + public Task Handle(GetCategoryBySlugQuery request, CancellationToken ct) => + repo.FirstOrDefaultAsync(new CategoryBySlugSpec(request.Slug), ct); +} \ No newline at end of file diff --git a/src/Moonglade.Core/CategoryFeature/GetCategoryQuery.cs b/src/Moonglade.Core/CategoryFeature/GetCategoryQuery.cs index 844ae87db..86bd62750 100644 --- a/src/Moonglade.Core/CategoryFeature/GetCategoryQuery.cs +++ b/src/Moonglade.Core/CategoryFeature/GetCategoryQuery.cs @@ -1,11 +1,11 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; namespace Moonglade.Core.CategoryFeature; -public record GetCategoryQuery(Guid Id) : IRequest; +public record GetCategoryQuery(Guid Id) : IRequest; -public class GetCategoryByIdQueryHandler(IRepository repo) : IRequestHandler +public class GetCategoryByIdQueryHandler(MoongladeRepository repo) : IRequestHandler { - public Task Handle(GetCategoryQuery request, CancellationToken ct) => - repo.FirstOrDefaultAsync(new CategorySpec(request.Id), Category.EntitySelector); + public async Task Handle(GetCategoryQuery request, CancellationToken ct) => + await repo.GetByIdAsync(request.Id, ct); } \ No newline at end of file diff --git a/src/Moonglade.Core/CategoryFeature/UpdateCategoryCommand.cs b/src/Moonglade.Core/CategoryFeature/UpdateCategoryCommand.cs index 4aa0ecc5d..19720f0f8 100644 --- a/src/Moonglade.Core/CategoryFeature/UpdateCategoryCommand.cs +++ b/src/Moonglade.Core/CategoryFeature/UpdateCategoryCommand.cs @@ -1,4 +1,5 @@ using Edi.CacheAside.InMemory; +using Microsoft.Extensions.Logging; using Moonglade.Data; namespace Moonglade.Core.CategoryFeature; @@ -8,20 +9,24 @@ public class UpdateCategoryCommand : CreateCategoryCommand, IRequest repo, ICacheAside cache) : IRequestHandler +public class UpdateCategoryCommandHandler( + MoongladeRepository repo, + ICacheAside cache, + ILogger logger) : IRequestHandler { public async Task Handle(UpdateCategoryCommand request, CancellationToken ct) { - var cat = await repo.GetAsync(request.Id, ct); + var cat = await repo.GetByIdAsync(request.Id, ct); if (cat is null) return OperationCode.ObjectNotFound; - cat.RouteName = request.RouteName.Trim(); + cat.Slug = request.Slug.Trim(); cat.DisplayName = request.DisplayName.Trim(); cat.Note = request.Note?.Trim(); await repo.UpdateAsync(cat, ct); cache.Remove(BlogCachePartition.General.ToString(), "allcats"); + logger.LogInformation("Category updated: {Category}", cat.Id); return OperationCode.Done; } } \ No newline at end of file diff --git a/src/Moonglade.Core/DeleteStyleSheetCommand.cs b/src/Moonglade.Core/DeleteStyleSheetCommand.cs index ddb17a4f3..b74ebe9b5 100644 --- a/src/Moonglade.Core/DeleteStyleSheetCommand.cs +++ b/src/Moonglade.Core/DeleteStyleSheetCommand.cs @@ -1,17 +1,19 @@ -namespace Moonglade.Core; +using Moonglade.Data; + +namespace Moonglade.Core; public record DeleteStyleSheetCommand(Guid Id) : IRequest; -public class DeleteStyleSheetCommandHandler(IRepository repo) : IRequestHandler +public class DeleteStyleSheetCommandHandler(MoongladeRepository repo) : IRequestHandler { - public async Task Handle(DeleteStyleSheetCommand request, CancellationToken cancellationToken) + public async Task Handle(DeleteStyleSheetCommand request, CancellationToken ct) { - var styleSheet = await repo.GetAsync(request.Id, cancellationToken); + var styleSheet = await repo.GetByIdAsync(request.Id, ct); if (styleSheet is null) { throw new InvalidOperationException($"StyleSheetEntity with Id '{request.Id}' not found."); } - await repo.DeleteAsync(styleSheet, cancellationToken); + await repo.DeleteAsync(styleSheet, ct); } } \ No newline at end of file diff --git a/src/Moonglade.Core/GetAssetQuery.cs b/src/Moonglade.Core/GetAssetQuery.cs index a35178522..bc055d2bd 100644 --- a/src/Moonglade.Core/GetAssetQuery.cs +++ b/src/Moonglade.Core/GetAssetQuery.cs @@ -1,12 +1,14 @@ -namespace Moonglade.Core; +using Moonglade.Data; + +namespace Moonglade.Core; public record GetAssetQuery(Guid AssetId) : IRequest; -public class GetAssetQueryHandler(IRepository repo) : IRequestHandler +public class GetAssetQueryHandler(MoongladeRepository repo) : IRequestHandler { public async Task Handle(GetAssetQuery request, CancellationToken ct) { - var asset = await repo.GetAsync(request.AssetId, ct); + var asset = await repo.GetByIdAsync(request.AssetId, ct); return asset?.Base64Data; } } \ No newline at end of file diff --git a/src/Moonglade.Core/GetStyleSheetQuery.cs b/src/Moonglade.Core/GetStyleSheetQuery.cs index 4b73e42d9..8730db874 100644 --- a/src/Moonglade.Core/GetStyleSheetQuery.cs +++ b/src/Moonglade.Core/GetStyleSheetQuery.cs @@ -1,12 +1,14 @@ -namespace Moonglade.Core; +using Moonglade.Data; + +namespace Moonglade.Core; public record GetStyleSheetQuery(Guid Id) : IRequest; -public class GetStyleSheetQueryHandler(IRepository repo) : IRequestHandler +public class GetStyleSheetQueryHandler(MoongladeRepository repo) : IRequestHandler { public async Task Handle(GetStyleSheetQuery request, CancellationToken cancellationToken) { - var result = await repo.GetAsync(request.Id, cancellationToken); + var result = await repo.GetByIdAsync(request.Id, cancellationToken); return result; } } \ No newline at end of file diff --git a/src/Moonglade.Core/Moonglade.Core.csproj b/src/Moonglade.Core/Moonglade.Core.csproj index c1e26fed7..b0f009de1 100644 --- a/src/Moonglade.Core/Moonglade.Core.csproj +++ b/src/Moonglade.Core/Moonglade.Core.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Moonglade.Core/PageFeature/BlogPage.cs b/src/Moonglade.Core/PageFeature/BlogPage.cs deleted file mode 100644 index 4ace8f98a..000000000 --- a/src/Moonglade.Core/PageFeature/BlogPage.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Moonglade.Core.PageFeature; - -public class BlogPage -{ - public Guid Id { get; set; } - public string Title { get; set; } - public string Slug { get; set; } - public string MetaDescription { get; set; } - public string RawHtmlContent { get; set; } - public string CssId { get; set; } - public bool HideSidebar { get; set; } - public bool IsPublished { get; set; } - public DateTime CreateTimeUtc { get; set; } - public DateTime? UpdateTimeUtc { get; set; } - - public BlogPage() - { - - } - - public BlogPage(PageEntity entity) - { - if (entity is null) return; - - Id = entity.Id; - Title = entity.Title.Trim(); - CreateTimeUtc = entity.CreateTimeUtc; - CssId = entity.CssId; - RawHtmlContent = entity.HtmlContent; - HideSidebar = entity.HideSidebar; - Slug = entity.Slug.Trim().ToLower(); - MetaDescription = entity.MetaDescription?.Trim(); - UpdateTimeUtc = entity.UpdateTimeUtc; - IsPublished = entity.IsPublished; - } -} \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/CreatePageCommand.cs b/src/Moonglade.Core/PageFeature/CreatePageCommand.cs index 2bc914242..7b8e5a1bd 100644 --- a/src/Moonglade.Core/PageFeature/CreatePageCommand.cs +++ b/src/Moonglade.Core/PageFeature/CreatePageCommand.cs @@ -1,8 +1,14 @@ -namespace Moonglade.Core.PageFeature; +using Microsoft.Extensions.Logging; +using Moonglade.Data; + +namespace Moonglade.Core.PageFeature; public record CreatePageCommand(EditPageRequest Payload) : IRequest; -public class CreatePageCommandHandler(IRepository repo, IMediator mediator) : IRequestHandler +public class CreatePageCommandHandler( + MoongladeRepository repo, + IMediator mediator, + ILogger logger) : IRequestHandler { public async Task Handle(CreatePageCommand request, CancellationToken ct) { @@ -22,6 +28,7 @@ public async Task Handle(CreatePageCommand request, CancellationToken ct) Slug = slug, MetaDescription = request.Payload.MetaDescription, CreateTimeUtc = DateTime.UtcNow, + UpdateTimeUtc = DateTime.UtcNow, HtmlContent = request.Payload.RawHtmlContent, HideSidebar = request.Payload.HideSidebar, IsPublished = request.Payload.IsPublished, @@ -30,6 +37,7 @@ public async Task Handle(CreatePageCommand request, CancellationToken ct) await repo.AddAsync(page, ct); + logger.LogInformation("Created page: {PageId}", uid); return uid; } } \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/DeletePageCommand.cs b/src/Moonglade.Core/PageFeature/DeletePageCommand.cs index 0651579e4..4e137cd7a 100644 --- a/src/Moonglade.Core/PageFeature/DeletePageCommand.cs +++ b/src/Moonglade.Core/PageFeature/DeletePageCommand.cs @@ -1,22 +1,28 @@ -namespace Moonglade.Core.PageFeature; +using Microsoft.Extensions.Logging; +using Moonglade.Data; -public record DeletePageCommand(Guid Id) : IRequest; +namespace Moonglade.Core.PageFeature; -public class DeletePageCommandHandler(IRepository repo, IMediator mediator) : IRequestHandler +public record DeletePageCommand(Guid Id) : IRequest; + +public class DeletePageCommandHandler( + MoongladeRepository repo, + IMediator mediator, + ILogger logger) : IRequestHandler { - public async Task Handle(DeletePageCommand request, CancellationToken ct) + public async Task Handle(DeletePageCommand request, CancellationToken ct) { - var page = await repo.GetAsync(request.Id, ct); - if (page is null) - { - throw new InvalidOperationException($"PageEntity with Id '{request.Id}' not found."); - } + var page = await repo.GetByIdAsync(request.Id, ct); + if (page == null) return OperationCode.ObjectNotFound; if (page.CssId != null) { await mediator.Send(new DeleteStyleSheetCommand(new(page.CssId)), ct); } - await repo.DeleteAsync(request.Id, ct); + await repo.DeleteAsync(page, ct); + + logger.LogInformation("Deleted page: {PageId}", request.Id); + return OperationCode.Done; } } \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/GetPageByIdQuery.cs b/src/Moonglade.Core/PageFeature/GetPageByIdQuery.cs index 9213c5a73..9634f30a7 100644 --- a/src/Moonglade.Core/PageFeature/GetPageByIdQuery.cs +++ b/src/Moonglade.Core/PageFeature/GetPageByIdQuery.cs @@ -1,15 +1,10 @@ -namespace Moonglade.Core.PageFeature; +using Moonglade.Data; -public record GetPageByIdQuery(Guid Id) : IRequest; +namespace Moonglade.Core.PageFeature; -public class GetPageByIdQueryHandler(IRepository repo) : IRequestHandler -{ - public async Task Handle(GetPageByIdQuery request, CancellationToken ct) - { - var entity = await repo.GetAsync(request.Id, ct); - if (entity == null) return null; +public record GetPageByIdQuery(Guid Id) : IRequest; - var item = new BlogPage(entity); - return item; - } +public class GetPageByIdQueryHandler(MoongladeRepository repo) : IRequestHandler +{ + public Task Handle(GetPageByIdQuery request, CancellationToken ct) => repo.GetByIdAsync(request.Id, ct); } \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/GetPageBySlugQuery.cs b/src/Moonglade.Core/PageFeature/GetPageBySlugQuery.cs index 73afd9377..864761409 100644 --- a/src/Moonglade.Core/PageFeature/GetPageBySlugQuery.cs +++ b/src/Moonglade.Core/PageFeature/GetPageBySlugQuery.cs @@ -1,16 +1,16 @@ -namespace Moonglade.Core.PageFeature; +using Moonglade.Data; +using Moonglade.Data.Specifications; -public record GetPageBySlugQuery(string Slug) : IRequest; +namespace Moonglade.Core.PageFeature; -public class GetPageBySlugQueryHandler(IRepository repo) : IRequestHandler +public record GetPageBySlugQuery(string Slug) : IRequest; + +public class GetPageBySlugQueryHandler(MoongladeRepository repo) : IRequestHandler { - public async Task Handle(GetPageBySlugQuery request, CancellationToken ct) + public async Task Handle(GetPageBySlugQuery request, CancellationToken ct) { var lower = request.Slug.ToLower(); - var entity = await repo.GetAsync(p => p.Slug == lower); - if (entity == null) return null; - - var item = new BlogPage(entity); - return item; + var entity = await repo.FirstOrDefaultAsync(new PageBySlugSpec(lower), ct); + return entity; } } \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/GetPagesQuery.cs b/src/Moonglade.Core/PageFeature/GetPagesQuery.cs deleted file mode 100644 index 85e348f01..000000000 --- a/src/Moonglade.Core/PageFeature/GetPagesQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Moonglade.Data.Spec; - -namespace Moonglade.Core.PageFeature; - -public record GetPagesQuery(int Top) : IRequest>; - -public class GetPagesQueryHandler(IRepository repo) : IRequestHandler> -{ - public async Task> Handle(GetPagesQuery request, CancellationToken ct) - { - var pages = await repo.ListAsync(new PageSpec(request.Top)); - var list = pages.Select(p => new BlogPage(p)).ToList(); - return list; - } -} \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/ListPageSegmentQuery.cs b/src/Moonglade.Core/PageFeature/ListPageSegmentQuery.cs index a8a0f2873..99cc00af1 100644 --- a/src/Moonglade.Core/PageFeature/ListPageSegmentQuery.cs +++ b/src/Moonglade.Core/PageFeature/ListPageSegmentQuery.cs @@ -1,18 +1,12 @@ -namespace Moonglade.Core.PageFeature; +using Moonglade.Data; +using Moonglade.Data.Specifications; -public record ListPageSegmentQuery : IRequest>; +namespace Moonglade.Core.PageFeature; -public class ListPageSegmentQueryHandler(IRepository repo) : IRequestHandler> +public record ListPageSegmentQuery : IRequest>; + +public class ListPageSegmentQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(ListPageSegmentQuery request, CancellationToken ct) - { - return repo.SelectAsync(page => new PageSegment - { - Id = page.Id, - CreateTimeUtc = page.CreateTimeUtc, - Slug = page.Slug, - Title = page.Title, - IsPublished = page.IsPublished - }, ct); - } + public Task> Handle(ListPageSegmentQuery request, CancellationToken ct) => + repo.ListAsync(new PageSegmentSpec(), ct); } \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/UpdatePageCommand.cs b/src/Moonglade.Core/PageFeature/UpdatePageCommand.cs index 7c7cac140..68a22546f 100644 --- a/src/Moonglade.Core/PageFeature/UpdatePageCommand.cs +++ b/src/Moonglade.Core/PageFeature/UpdatePageCommand.cs @@ -1,13 +1,19 @@ -namespace Moonglade.Core.PageFeature; +using Microsoft.Extensions.Logging; +using Moonglade.Data; + +namespace Moonglade.Core.PageFeature; public record UpdatePageCommand(Guid Id, EditPageRequest Payload) : IRequest; -public class UpdatePageCommandHandler(IRepository repo, IMediator mediator) : IRequestHandler +public class UpdatePageCommandHandler( + MoongladeRepository repo, + IMediator mediator, + ILogger logger) : IRequestHandler { public async Task Handle(UpdatePageCommand request, CancellationToken ct) { var (guid, payload) = request; - var page = await repo.GetAsync(guid, ct); + var page = await repo.GetByIdAsync(guid, ct); if (page is null) { throw new InvalidOperationException($"PageEntity with Id '{guid}' not found."); @@ -32,6 +38,7 @@ public async Task Handle(UpdatePageCommand request, CancellationToken ct) await repo.UpdateAsync(page, ct); + logger.LogInformation("Page updated: {PageId}", page.Id); return page.Id; } } \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/CountPostQuery.cs b/src/Moonglade.Core/PostFeature/CountPostQuery.cs index 6a70cb4e8..ce7962881 100644 --- a/src/Moonglade.Core/PostFeature/CountPostQuery.cs +++ b/src/Moonglade.Core/PostFeature/CountPostQuery.cs @@ -1,4 +1,7 @@ -namespace Moonglade.Core.PostFeature; +using Moonglade.Data; +using Moonglade.Data.Specifications; + +namespace Moonglade.Core.PostFeature; public enum CountType { @@ -10,9 +13,10 @@ public enum CountType public record CountPostQuery(CountType CountType, Guid? CatId = null, int? TagId = null) : IRequest; -public class CountPostQueryHandler(IRepository postRepo, - IRepository postTagRepo, - IRepository postCatRepo) +public class CountPostQueryHandler( + MoongladeRepository postRepo, + MoongladeRepository postTagRepo, + MoongladeRepository postCatRepo) : IRequestHandler { public async Task Handle(CountPostQuery request, CancellationToken ct) @@ -22,7 +26,7 @@ public async Task Handle(CountPostQuery request, CancellationToken ct) switch (request.CountType) { case CountType.Public: - count = await postRepo.CountAsync(p => p.IsPublished && !p.IsDeleted, ct); + count = await postRepo.CountAsync(new PostByStatusSpec(PostStatus.Published), ct); break; case CountType.Category: @@ -38,7 +42,7 @@ public async Task Handle(CountPostQuery request, CancellationToken ct) break; case CountType.Featured: - count = await postRepo.CountAsync(p => p.IsFeatured && p.IsPublished && !p.IsDeleted, ct); + count = await postRepo.CountAsync(new FeaturedPostSpec(), ct); break; } diff --git a/src/Moonglade.Core/PostFeature/CreatePostCommand.cs b/src/Moonglade.Core/PostFeature/CreatePostCommand.cs index 43c53dc7f..21e409f60 100644 --- a/src/Moonglade.Core/PostFeature/CreatePostCommand.cs +++ b/src/Moonglade.Core/PostFeature/CreatePostCommand.cs @@ -1,17 +1,18 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Moonglade.Configuration; -using Moonglade.Core.TagFeature; -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; using Moonglade.Utils; namespace Moonglade.Core.PostFeature; public record CreatePostCommand(PostEditModel Payload) : IRequest; -public class CreatePostCommandHandler(IRepository postRepo, +public class CreatePostCommandHandler( + MoongladeRepository postRepo, + MoongladeRepository tagRepo, ILogger logger, - IRepository tagRepo, IConfiguration configuration, IBlogConfig blogConfig) : IRequestHandler @@ -48,7 +49,7 @@ public async Task Handle(CreatePostCommand request, CancellationToke // check if exist same slug under the same day var todayUtc = DateTime.UtcNow.Date; - if (await postRepo.AnyAsync(new PostSpec(post.Slug, todayUtc), ct)) + if (await postRepo.AnyAsync(new PostByDateAndSlugSpec(todayUtc, post.Slug, false), ct)) { var uid = Guid.NewGuid(); post.Slug += $"-{uid.ToString().ToLower()[..8]}"; @@ -82,15 +83,16 @@ public async Task Handle(CreatePostCommand request, CancellationToke { foreach (var item in tags) { - if (!Tag.ValidateName(item)) continue; + if (!Helper.IsValidTagName(item)) continue; - var tag = await tagRepo.GetAsync(q => q.DisplayName == item) ?? await CreateTag(item); + var tag = await tagRepo.FirstOrDefaultAsync(new TagByDisplayNameSpec(item), ct) ?? await CreateTag(item); post.Tags.Add(tag); } } await postRepo.AddAsync(post, ct); + logger.LogInformation($"Created post Id: {post.Id}, Title: '{post.Title}'"); return post; } @@ -99,10 +101,12 @@ private async Task CreateTag(string item) var newTag = new TagEntity { DisplayName = item, - NormalizedName = Tag.NormalizeName(item, Helper.TagNormalizationDictionary) + NormalizedName = Helper.NormalizeName(item, Helper.TagNormalizationDictionary) }; var tag = await tagRepo.AddAsync(newTag); + + logger.LogInformation($"Created tag: {tag.DisplayName}"); return tag; } } \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/DeletePostCommand.cs b/src/Moonglade.Core/PostFeature/DeletePostCommand.cs index e17a5451d..d201c3a43 100644 --- a/src/Moonglade.Core/PostFeature/DeletePostCommand.cs +++ b/src/Moonglade.Core/PostFeature/DeletePostCommand.cs @@ -1,15 +1,16 @@ using Edi.CacheAside.InMemory; +using Moonglade.Data; namespace Moonglade.Core.PostFeature; public record DeletePostCommand(Guid Id, bool SoftDelete = false) : IRequest; -public class DeletePostCommandHandler(IRepository repo, ICacheAside cache) : IRequestHandler +public class DeletePostCommandHandler(MoongladeRepository repo, ICacheAside cache) : IRequestHandler { public async Task Handle(DeletePostCommand request, CancellationToken ct) { var (guid, softDelete) = request; - var post = await repo.GetAsync(guid, ct); + var post = await repo.GetByIdAsync(guid, ct); if (null == post) return; if (softDelete) diff --git a/src/Moonglade.Core/PostFeature/GetArchiveQuery.cs b/src/Moonglade.Core/PostFeature/GetArchiveQuery.cs index 323d4df92..02cc33d74 100644 --- a/src/Moonglade.Core/PostFeature/GetArchiveQuery.cs +++ b/src/Moonglade.Core/PostFeature/GetArchiveQuery.cs @@ -1,24 +1,25 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; using System.Linq.Expressions; namespace Moonglade.Core.PostFeature; public record struct Archive(int Year, int Month, int Count); -public record GetArchiveQuery : IRequest>; +public record GetArchiveQuery : IRequest>; -public class GetArchiveQueryHandler(IRepository repo) : IRequestHandler> +public class GetArchiveQueryHandler(MoongladeRepository repo) : IRequestHandler> { private readonly Expression, Archive>> _archiveSelector = p => new(p.Key.Year, p.Key.Month, p.Count()); - public async Task> Handle(GetArchiveQuery request, CancellationToken ct) + public async Task> Handle(GetArchiveQuery request, CancellationToken ct) { - if (!await repo.AnyAsync(p => p.IsPublished && !p.IsDeleted, ct)) + if (!await repo.AnyAsync(new PostByStatusSpec(PostStatus.Published), ct)) { - return new List(); + return []; } - var spec = new PostSpec(PostStatus.Published); + var spec = new PostByStatusSpec(PostStatus.Published); var list = await repo.SelectAsync( post => new(post.PubDateUtc.Value.Year, post.PubDateUtc.Value.Month), _archiveSelector, spec); diff --git a/src/Moonglade.Core/PostFeature/GetDraftQuery.cs b/src/Moonglade.Core/PostFeature/GetDraftQuery.cs index 02051396a..c308eacb6 100644 --- a/src/Moonglade.Core/PostFeature/GetDraftQuery.cs +++ b/src/Moonglade.Core/PostFeature/GetDraftQuery.cs @@ -1,15 +1,16 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; namespace Moonglade.Core.PostFeature; -public record GetDraftQuery(Guid Id) : IRequest; +public record GetDraftQuery(Guid Id) : IRequest; -public class GetDraftQueryHandler(IRepository repo) : IRequestHandler +public class GetDraftQueryHandler(MoongladeRepository repo) : IRequestHandler { - public Task Handle(GetDraftQuery request, CancellationToken ct) + public Task Handle(GetDraftQuery request, CancellationToken ct) { var spec = new PostSpec(request.Id); - var post = repo.FirstOrDefaultAsync(spec, Post.EntitySelector); + var post = repo.FirstOrDefaultAsync(spec, ct); return post; } } \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/GetPostByIdQuery.cs b/src/Moonglade.Core/PostFeature/GetPostByIdQuery.cs index 628dd4d56..9935717cb 100644 --- a/src/Moonglade.Core/PostFeature/GetPostByIdQuery.cs +++ b/src/Moonglade.Core/PostFeature/GetPostByIdQuery.cs @@ -1,15 +1,16 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; namespace Moonglade.Core.PostFeature; -public record GetPostByIdQuery(Guid Id) : IRequest; +public record GetPostByIdQuery(Guid Id) : IRequest; -public class GetPostByIdQueryHandler(IRepository repo) : IRequestHandler +public class GetPostByIdQueryHandler(MoongladeRepository repo) : IRequestHandler { - public Task Handle(GetPostByIdQuery request, CancellationToken ct) + public Task Handle(GetPostByIdQuery request, CancellationToken ct) { var spec = new PostSpec(request.Id); - var post = repo.FirstOrDefaultAsync(spec, Post.EntitySelector); + var post = repo.FirstOrDefaultAsync(spec, ct); return post; } } \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/GetPostBySlugQuery.cs b/src/Moonglade.Core/PostFeature/GetPostBySlugQuery.cs index bd464dcee..cba55232f 100644 --- a/src/Moonglade.Core/PostFeature/GetPostBySlugQuery.cs +++ b/src/Moonglade.Core/PostFeature/GetPostBySlugQuery.cs @@ -1,44 +1,29 @@ using Edi.CacheAside.InMemory; using Microsoft.Extensions.Configuration; -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; using Moonglade.Utils; namespace Moonglade.Core.PostFeature; -public record GetPostBySlugQuery(PostSlug Slug) : IRequest; +public record GetPostBySlugQuery(PostSlug Slug) : IRequest; -public class GetPostBySlugQueryHandler(IRepository repo, ICacheAside cache, IConfiguration configuration) - : IRequestHandler +public class GetPostBySlugQueryHandler(MoongladeRepository repo, ICacheAside cache, IConfiguration configuration) + : IRequestHandler { - public async Task Handle(GetPostBySlugQuery request, CancellationToken ct) + public async Task Handle(GetPostBySlugQuery request, CancellationToken ct) { var date = new DateTime(request.Slug.Year, request.Slug.Month, request.Slug.Day); // Try to find by checksum var slugCheckSum = Helper.ComputeCheckSum($"{request.Slug.Slug}#{date:yyyyMMdd}"); - ISpecification spec = new PostSpec(slugCheckSum); + var spec = new PostByChecksumSpec(slugCheckSum); - var pid = await repo.FirstOrDefaultAsync(spec, p => p.Id); - if (pid == Guid.Empty) - { - // Post does not have a checksum, fall back to old method - spec = new PostSpec(date, request.Slug.Slug); - pid = await repo.FirstOrDefaultAsync(spec, x => x.Id); - - if (pid == Guid.Empty) return null; - - // Post is found, fill it's checksum so that next time the query can be run against checksum - var p = await repo.GetAsync(pid, ct); - p.HashCheckSum = slugCheckSum; - - await repo.UpdateAsync(p, ct); - } - - var psm = await cache.GetOrCreateAsync(BlogCachePartition.Post.ToString(), $"{pid}", async entry => + var psm = await cache.GetOrCreateAsync(BlogCachePartition.Post.ToString(), $"{slugCheckSum}", async entry => { entry.SlidingExpiration = TimeSpan.FromMinutes(int.Parse(configuration["CacheSlidingExpirationMinutes:Post"]!)); - var post = await repo.FirstOrDefaultAsync(spec, Post.EntitySelector); + var post = await repo.FirstOrDefaultAsync(spec, ct); return post; }); diff --git a/src/Moonglade.Core/PostFeature/ListArchiveQuery.cs b/src/Moonglade.Core/PostFeature/ListArchiveQuery.cs index 72de4f0a7..d5b5e7d1b 100644 --- a/src/Moonglade.Core/PostFeature/ListArchiveQuery.cs +++ b/src/Moonglade.Core/PostFeature/ListArchiveQuery.cs @@ -1,14 +1,15 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; namespace Moonglade.Core.PostFeature; -public record ListArchiveQuery(int Year, int? Month = null) : IRequest>; +public record ListArchiveQuery(int Year, int? Month = null) : IRequest>; -public class ListArchiveQueryHandler(IRepository repo) : IRequestHandler> +public class ListArchiveQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(ListArchiveQuery request, CancellationToken ct) + public Task> Handle(ListArchiveQuery request, CancellationToken ct) { - var spec = new PostSpec(request.Year, request.Month.GetValueOrDefault()); + var spec = new PostByYearMonthSpec(request.Year, request.Month.GetValueOrDefault()); var list = repo.SelectAsync(spec, PostDigest.EntitySelector, ct); return list; } diff --git a/src/Moonglade.Core/PostFeature/ListByTagQuery.cs b/src/Moonglade.Core/PostFeature/ListByTagQuery.cs index d21f0f7ae..8e46c2a3e 100644 --- a/src/Moonglade.Core/PostFeature/ListByTagQuery.cs +++ b/src/Moonglade.Core/PostFeature/ListByTagQuery.cs @@ -1,13 +1,14 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; using Moonglade.Utils; namespace Moonglade.Core.PostFeature; -public record ListByTagQuery(int TagId, int PageSize, int PageIndex) : IRequest>; +public record ListByTagQuery(int TagId, int PageSize, int PageIndex) : IRequest>; -public class ListByTagQueryHandler(IRepository repo) : IRequestHandler> +public class ListByTagQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(ListByTagQuery request, CancellationToken ct) + public Task> Handle(ListByTagQuery request, CancellationToken ct) { if (request.TagId <= 0) throw new ArgumentOutOfRangeException(nameof(request.TagId)); Helper.ValidatePagingParameters(request.PageSize, request.PageIndex); diff --git a/src/Moonglade.Core/PostFeature/ListFeaturedQuery.cs b/src/Moonglade.Core/PostFeature/ListFeaturedQuery.cs index 59c7230bf..d81721348 100644 --- a/src/Moonglade.Core/PostFeature/ListFeaturedQuery.cs +++ b/src/Moonglade.Core/PostFeature/ListFeaturedQuery.cs @@ -1,18 +1,19 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; using Moonglade.Utils; namespace Moonglade.Core.PostFeature; -public record ListFeaturedQuery(int PageSize, int PageIndex) : IRequest>; +public record ListFeaturedQuery(int PageSize, int PageIndex) : IRequest>; -public class ListFeaturedQueryHandler(IRepository repo) : IRequestHandler> +public class ListFeaturedQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(ListFeaturedQuery request, CancellationToken ct) + public Task> Handle(ListFeaturedQuery request, CancellationToken ct) { var (pageSize, pageIndex) = request; Helper.ValidatePagingParameters(pageSize, pageIndex); - var posts = repo.SelectAsync(new FeaturedPostSpec(pageSize, pageIndex), PostDigest.EntitySelector, ct); + var posts = repo.SelectAsync(new FeaturedPostPagingSpec(pageSize, pageIndex), PostDigest.EntitySelector, ct); return posts; } } \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/ListPostSegmentByStatusQuery.cs b/src/Moonglade.Core/PostFeature/ListPostSegmentByStatusQuery.cs index f8df8252b..395da9c43 100644 --- a/src/Moonglade.Core/PostFeature/ListPostSegmentByStatusQuery.cs +++ b/src/Moonglade.Core/PostFeature/ListPostSegmentByStatusQuery.cs @@ -1,14 +1,15 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; namespace Moonglade.Core.PostFeature; -public record ListPostSegmentByStatusQuery(PostStatus Status) : IRequest>; +public record ListPostSegmentByStatusQuery(PostStatus Status) : IRequest>; -public class ListPostSegmentByStatusQueryHandler(IRepository repo) : IRequestHandler> +public class ListPostSegmentByStatusQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(ListPostSegmentByStatusQuery request, CancellationToken ct) + public Task> Handle(ListPostSegmentByStatusQuery request, CancellationToken ct) { - var spec = new PostSpec(request.Status); + var spec = new PostByStatusSpec(request.Status); return repo.SelectAsync(spec, PostSegment.EntitySelector, ct); } } \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/ListPostSegmentQuery.cs b/src/Moonglade.Core/PostFeature/ListPostSegmentQuery.cs index e12212aeb..ca077ac0e 100644 --- a/src/Moonglade.Core/PostFeature/ListPostSegmentQuery.cs +++ b/src/Moonglade.Core/PostFeature/ListPostSegmentQuery.cs @@ -1,10 +1,11 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; using System.Linq.Expressions; namespace Moonglade.Core.PostFeature; public class ListPostSegmentQuery(PostStatus postStatus, int offset, int pageSize, string keyword = null) - : IRequest<(IReadOnlyList Posts, int TotalRows)> + : IRequest<(List Posts, int TotalRows)> { public PostStatus PostStatus { get; set; } = postStatus; @@ -15,9 +16,10 @@ public class ListPostSegmentQuery(PostStatus postStatus, int offset, int pageSiz public string Keyword { get; set; } = keyword; } -public class ListPostSegmentQueryHandler(IRepository repo) : IRequestHandler Posts, int TotalRows)> +public class ListPostSegmentQueryHandler(MoongladeRepository repo) : + IRequestHandler Posts, int TotalRows)> { - public async Task<(IReadOnlyList Posts, int TotalRows)> Handle(ListPostSegmentQuery request, CancellationToken ct) + public async Task<(List Posts, int TotalRows)> Handle(ListPostSegmentQuery request, CancellationToken ct) { if (request.PageSize < 1) { @@ -30,7 +32,7 @@ public class ListPostSegmentQueryHandler(IRepository repo) : IReques $"{nameof(request.Offset)} can not be less than 0, current value: {request.Offset}."); } - var spec = new PostPagingSpec(request.PostStatus, request.Keyword, request.PageSize, request.Offset); + var spec = new PostPagingByStatusSpec(request.PostStatus, request.Keyword, request.PageSize, request.Offset); var posts = await repo.SelectAsync(spec, PostSegment.EntitySelector, ct); Expression> countExp = p => null == request.Keyword || p.Title.Contains(request.Keyword); diff --git a/src/Moonglade.Core/PostFeature/ListPostsQuery.cs b/src/Moonglade.Core/PostFeature/ListPostsQuery.cs index 968e4593f..69d630f8e 100644 --- a/src/Moonglade.Core/PostFeature/ListPostsQuery.cs +++ b/src/Moonglade.Core/PostFeature/ListPostsQuery.cs @@ -1,10 +1,11 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; using Moonglade.Utils; namespace Moonglade.Core.PostFeature; public class ListPostsQuery(int pageSize, int pageIndex, Guid? catId = null) - : IRequest> + : IRequest> { public int PageSize { get; set; } = pageSize; @@ -13,9 +14,9 @@ public class ListPostsQuery(int pageSize, int pageIndex, Guid? catId = null) public Guid? CatId { get; set; } = catId; } -public class ListPostsQueryHandler(IRepository repo) : IRequestHandler> +public class ListPostsQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(ListPostsQuery request, CancellationToken ct) + public Task> Handle(ListPostsQuery request, CancellationToken ct) { Helper.ValidatePagingParameters(request.PageSize, request.PageIndex); diff --git a/src/Moonglade.Core/PostFeature/Post.cs b/src/Moonglade.Core/PostFeature/Post.cs deleted file mode 100644 index 7da4393b2..000000000 --- a/src/Moonglade.Core/PostFeature/Post.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Moonglade.Core.CategoryFeature; -using Moonglade.Core.TagFeature; -using System.Linq.Expressions; - -namespace Moonglade.Core.PostFeature; - -public class Post -{ - public Guid Id { get; set; } - public string Title { get; set; } - public string Slug { get; set; } - public string Author { get; set; } - public string RawPostContent { get; set; } - public bool CommentEnabled { get; set; } - public DateTime CreateTimeUtc { get; set; } - public string ContentAbstract { get; set; } - public bool IsPublished { get; set; } - public bool IsFeedIncluded { get; set; } - public bool Featured { get; set; } - public string ContentLanguageCode { get; set; } - public bool IsOriginal { get; set; } - public string OriginLink { get; set; } - public string HeroImageUrl { get; set; } - public bool IsOutdated { get; set; } - public Tag[] Tags { get; set; } - public Category[] Categories { get; set; } - public DateTime? PubDateUtc { get; set; } - public DateTime? LastModifiedUtc { get; set; } - - public static readonly Expression> EntitySelector = p => new() - { - Id = p.Id, - Title = p.Title, - Slug = p.Slug, - Author = p.Author, - RawPostContent = p.PostContent, - ContentAbstract = p.ContentAbstract, - CommentEnabled = p.CommentEnabled, - CreateTimeUtc = p.CreateTimeUtc, - PubDateUtc = p.PubDateUtc, - LastModifiedUtc = p.LastModifiedUtc, - IsPublished = p.IsPublished, - IsFeedIncluded = p.IsFeedIncluded, - Featured = p.IsFeatured, - IsOriginal = p.IsOriginal, - OriginLink = p.OriginLink, - HeroImageUrl = p.HeroImageUrl, - IsOutdated = p.IsOutdated, - ContentLanguageCode = p.ContentLanguageCode, - Tags = p.Tags.Select(pt => new Tag - { - Id = pt.Id, - NormalizedName = pt.NormalizedName, - DisplayName = pt.DisplayName - }).ToArray(), - Categories = p.PostCategory.Select(pc => new Category - { - Id = pc.CategoryId, - DisplayName = pc.Category.DisplayName, - RouteName = pc.Category.RouteName, - Note = pc.Category.Note - }).ToArray() - }; -} \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/PurgeRecycledCommand.cs b/src/Moonglade.Core/PostFeature/PurgeRecycledCommand.cs index 2a5897145..af975bdd3 100644 --- a/src/Moonglade.Core/PostFeature/PurgeRecycledCommand.cs +++ b/src/Moonglade.Core/PostFeature/PurgeRecycledCommand.cs @@ -1,17 +1,18 @@ using Edi.CacheAside.InMemory; -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; namespace Moonglade.Core.PostFeature; public record PurgeRecycledCommand : IRequest; -public class PurgeRecycledCommandHandler(ICacheAside cache, IRepository repo) : IRequestHandler +public class PurgeRecycledCommandHandler(ICacheAside cache, MoongladeRepository repo) : IRequestHandler { public async Task Handle(PurgeRecycledCommand request, CancellationToken ct) { - var spec = new PostSpec(true); - var posts = await repo.ListAsync(spec); - await repo.DeleteAsync(posts, ct); + var spec = new PostByDeletionFlagSpec(true); + var posts = await repo.ListAsync(spec, ct); + await repo.DeleteRangeAsync(posts, ct); foreach (var guid in posts.Select(p => p.Id)) { diff --git a/src/Moonglade.Core/PostFeature/RestorePostCommand.cs b/src/Moonglade.Core/PostFeature/RestorePostCommand.cs index 818552d35..f928a6eee 100644 --- a/src/Moonglade.Core/PostFeature/RestorePostCommand.cs +++ b/src/Moonglade.Core/PostFeature/RestorePostCommand.cs @@ -1,14 +1,15 @@ using Edi.CacheAside.InMemory; +using Moonglade.Data; namespace Moonglade.Core.PostFeature; public record RestorePostCommand(Guid Id) : IRequest; -public class RestorePostCommandHandler(IRepository repo, ICacheAside cache) : IRequestHandler +public class RestorePostCommandHandler(MoongladeRepository repo, ICacheAside cache) : IRequestHandler { public async Task Handle(RestorePostCommand request, CancellationToken ct) { - var pp = await repo.GetAsync(request.Id, ct); + var pp = await repo.GetByIdAsync(request.Id, ct); if (null == pp) return; pp.IsDeleted = false; diff --git a/src/Moonglade.Core/PostFeature/SearchPostQuery.cs b/src/Moonglade.Core/PostFeature/SearchPostQuery.cs index 32bc6c98d..37ff3f4be 100644 --- a/src/Moonglade.Core/PostFeature/SearchPostQuery.cs +++ b/src/Moonglade.Core/PostFeature/SearchPostQuery.cs @@ -1,13 +1,14 @@ using Microsoft.EntityFrameworkCore; +using Moonglade.Data; using System.Text.RegularExpressions; namespace Moonglade.Core.PostFeature; -public record SearchPostQuery(string Keyword) : IRequest>; +public record SearchPostQuery(string Keyword) : IRequest>; -public class SearchPostQueryHandler(IRepository repo) : IRequestHandler> +public class SearchPostQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public async Task> Handle(SearchPostQuery request, CancellationToken ct) + public async Task> Handle(SearchPostQuery request, CancellationToken ct) { if (null == request || string.IsNullOrWhiteSpace(request.Keyword)) { diff --git a/src/Moonglade.Core/PostFeature/UpdatePostCommand.cs b/src/Moonglade.Core/PostFeature/UpdatePostCommand.cs index 8ef506dff..7aca0251b 100644 --- a/src/Moonglade.Core/PostFeature/UpdatePostCommand.cs +++ b/src/Moonglade.Core/PostFeature/UpdatePostCommand.cs @@ -1,8 +1,10 @@ using Edi.CacheAside.InMemory; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Moonglade.Configuration; -using Moonglade.Core.TagFeature; +using Moonglade.Data; +using Moonglade.Data.Specifications; using Moonglade.Utils; namespace Moonglade.Core.PostFeature; @@ -10,22 +12,25 @@ namespace Moonglade.Core.PostFeature; public record UpdatePostCommand(Guid Id, PostEditModel Payload) : IRequest; public class UpdatePostCommandHandler : IRequestHandler { - private readonly IRepository _pcRepository; - private readonly IRepository _ptRepository; - private readonly IRepository _tagRepo; - private readonly IRepository _postRepo; + private readonly MoongladeRepository _pcRepository; + private readonly MoongladeRepository _ptRepository; + private readonly MoongladeRepository _tagRepo; + private readonly MoongladeRepository _postRepo; private readonly ICacheAside _cache; private readonly IBlogConfig _blogConfig; private readonly IConfiguration _configuration; + private readonly ILogger _logger; private readonly bool _useMySqlWorkaround; public UpdatePostCommandHandler( - IRepository pcRepository, - IRepository ptRepository, - IRepository tagRepo, - IRepository postRepo, + MoongladeRepository pcRepository, + MoongladeRepository ptRepository, + MoongladeRepository tagRepo, + MoongladeRepository postRepo, ICacheAside cache, - IBlogConfig blogConfig, IConfiguration configuration) + IBlogConfig blogConfig, + IConfiguration configuration, + ILogger logger) { _ptRepository = ptRepository; _pcRepository = pcRepository; @@ -34,6 +39,7 @@ public UpdatePostCommandHandler( _cache = cache; _blogConfig = blogConfig; _configuration = configuration; + _logger = logger; string dbType = configuration.GetConnectionString("DatabaseType"); _useMySqlWorkaround = dbType!.ToLower().Trim() == "mysql"; @@ -42,7 +48,7 @@ public UpdatePostCommandHandler( public async Task Handle(UpdatePostCommand request, CancellationToken ct) { var (guid, postEditModel) = request; - var post = await _postRepo.GetAsync(guid, ct); + var post = await _postRepo.GetByIdAsync(guid, ct); if (null == post) { throw new InvalidOperationException($"Post {guid} is not found."); @@ -93,12 +99,12 @@ public async Task Handle(UpdatePostCommand request, CancellationToke foreach (var item in tags) { - if (!await _tagRepo.AnyAsync(p => p.DisplayName == item, ct)) + if (!await _tagRepo.AnyAsync(new TagByDisplayNameSpec(item), ct)) { await _tagRepo.AddAsync(new() { DisplayName = item, - NormalizedName = Tag.NormalizeName(item, Helper.TagNormalizationDictionary) + NormalizedName = Helper.NormalizeName(item, Helper.TagNormalizationDictionary) }, ct); } } @@ -107,7 +113,7 @@ await _tagRepo.AddAsync(new() if (_useMySqlWorkaround) { var oldTags = await _ptRepository.AsQueryable().Where(pc => pc.PostId == post.Id).ToListAsync(cancellationToken: ct); - await _ptRepository.DeleteAsync(oldTags, ct); + await _ptRepository.DeleteRangeAsync(oldTags, ct); } post.Tags.Clear(); @@ -115,12 +121,12 @@ await _tagRepo.AddAsync(new() { foreach (var tagName in tags) { - if (!Tag.ValidateName(tagName)) + if (!Helper.IsValidTagName(tagName)) { continue; } - var tag = await _tagRepo.GetAsync(t => t.DisplayName == tagName); + var tag = await _tagRepo.FirstOrDefaultAsync(new TagByDisplayNameSpec(tagName), ct); if (tag is not null) post.Tags.Add(tag); } } @@ -130,7 +136,7 @@ await _tagRepo.AddAsync(new() { var oldpcs = await _pcRepository.AsQueryable().Where(pc => pc.PostId == post.Id) .ToListAsync(cancellationToken: ct); - await _pcRepository.DeleteAsync(oldpcs, ct); + await _pcRepository.DeleteRangeAsync(oldpcs, ct); } post.PostCategory.Clear(); @@ -148,7 +154,9 @@ await _tagRepo.AddAsync(new() await _postRepo.UpdateAsync(post, ct); - _cache.Remove(BlogCachePartition.Post.ToString(), guid.ToString()); + _cache.Remove(BlogCachePartition.Post.ToString(), checkSum.ToString()); + + _logger.LogInformation($"Post updated: {post.Id}"); return post; } } diff --git a/src/Moonglade.Core/SaveAssetCommand.cs b/src/Moonglade.Core/SaveAssetCommand.cs index fefeb093f..ffc8588ee 100644 --- a/src/Moonglade.Core/SaveAssetCommand.cs +++ b/src/Moonglade.Core/SaveAssetCommand.cs @@ -1,15 +1,17 @@ -namespace Moonglade.Core; +using Moonglade.Data; + +namespace Moonglade.Core; public record SaveAssetCommand(Guid AssetId, string AssetBase64) : INotification; -public class SaveAssetCommandHandler(IRepository repo) : INotificationHandler +public class SaveAssetCommandHandler(MoongladeRepository repo) : INotificationHandler { public async Task Handle(SaveAssetCommand request, CancellationToken ct) { if (request.AssetId == Guid.Empty) throw new ArgumentOutOfRangeException(nameof(request.AssetId)); if (string.IsNullOrWhiteSpace(request.AssetBase64)) throw new ArgumentNullException(nameof(request.AssetBase64)); - var entity = await repo.GetAsync(request.AssetId, ct); + var entity = await repo.GetByIdAsync(request.AssetId, ct); if (null == entity) { diff --git a/src/Moonglade.Core/SaveStyleSheetCommand.cs b/src/Moonglade.Core/SaveStyleSheetCommand.cs index 0dd46cf30..126dcae9e 100644 --- a/src/Moonglade.Core/SaveStyleSheetCommand.cs +++ b/src/Moonglade.Core/SaveStyleSheetCommand.cs @@ -1,10 +1,11 @@ -using System.Security.Cryptography; +using Moonglade.Data; +using System.Security.Cryptography; namespace Moonglade.Core; public record SaveStyleSheetCommand(Guid Id, string Slug, string CssContent) : IRequest; -public class SaveStyleSheetCommandHandler(IRepository repo) : IRequestHandler +public class SaveStyleSheetCommandHandler(MoongladeRepository repo) : IRequestHandler { public async Task Handle(SaveStyleSheetCommand request, CancellationToken cancellationToken) { @@ -12,7 +13,7 @@ public async Task Handle(SaveStyleSheetCommand request, CancellationToken var css = request.CssContent.Trim(); var hash = CalculateHash($"{slug}_{css}"); - var entity = await repo.GetAsync(request.Id, cancellationToken); + var entity = await repo.GetByIdAsync(request.Id, cancellationToken); if (entity is null) { entity = new() diff --git a/src/Moonglade.Core/TagFeature/CreateTagCommand.cs b/src/Moonglade.Core/TagFeature/CreateTagCommand.cs index 44f9567df..37848fbd6 100644 --- a/src/Moonglade.Core/TagFeature/CreateTagCommand.cs +++ b/src/Moonglade.Core/TagFeature/CreateTagCommand.cs @@ -1,21 +1,22 @@ -using Moonglade.Data.Spec; +using Microsoft.Extensions.Logging; +using Moonglade.Data; +using Moonglade.Data.Specifications; using Moonglade.Utils; namespace Moonglade.Core.TagFeature; -public record CreateTagCommand(string Name) : IRequest; +public record CreateTagCommand(string Name) : IRequest; -public class CreateTagCommandHandler(IRepository repo) : IRequestHandler +public class CreateTagCommandHandler( + MoongladeRepository repo, + ILogger logger) : IRequestHandler { - public async Task Handle(CreateTagCommand request, CancellationToken ct) + public async Task Handle(CreateTagCommand request, CancellationToken ct) { - if (!Tag.ValidateName(request.Name)) return null; + var normalizedName = Helper.NormalizeName(request.Name, Helper.TagNormalizationDictionary); - var normalizedName = Tag.NormalizeName(request.Name, Helper.TagNormalizationDictionary); - if (await repo.AnyAsync(t => t.NormalizedName == normalizedName, ct)) - { - return await repo.FirstOrDefaultAsync(new TagSpec(normalizedName), Tag.EntitySelector); - } + var existingTag = await repo.FirstOrDefaultAsync(new TagByNormalizedNameSpec(normalizedName), ct); + if (null != existingTag) return; var newTag = new TagEntity { @@ -23,12 +24,7 @@ public async Task Handle(CreateTagCommand request, CancellationToken ct) NormalizedName = normalizedName }; - var tag = await repo.AddAsync(newTag, ct); - - return new() - { - DisplayName = tag.DisplayName, - NormalizedName = tag.NormalizedName - }; + await repo.AddAsync(newTag, ct); + logger.LogInformation("Tag created: {TagName}", request.Name); } } \ No newline at end of file diff --git a/src/Moonglade.Core/TagFeature/DeleteTagCommand.cs b/src/Moonglade.Core/TagFeature/DeleteTagCommand.cs index 920e8aed2..324a32474 100644 --- a/src/Moonglade.Core/TagFeature/DeleteTagCommand.cs +++ b/src/Moonglade.Core/TagFeature/DeleteTagCommand.cs @@ -1,25 +1,30 @@ -using Moonglade.Data; -using Moonglade.Data.Spec; +using Microsoft.Extensions.Logging; +using Moonglade.Data; +using Moonglade.Data.Specifications; namespace Moonglade.Core.TagFeature; public record DeleteTagCommand(int Id) : IRequest; -public class DeleteTagCommandHandler(IRepository tagRepo, IRepository postTagRepo) +public class DeleteTagCommandHandler( + MoongladeRepository tagRepo, + MoongladeRepository postTagRepo, + ILogger logger) : IRequestHandler { public async Task Handle(DeleteTagCommand request, CancellationToken ct) { - var exists = await tagRepo.AnyAsync(c => c.Id == request.Id, ct); - if (!exists) return OperationCode.ObjectNotFound; + var tag = await tagRepo.GetByIdAsync(request.Id, ct); + if (null == tag) return OperationCode.ObjectNotFound; // 1. Delete Post-Tag Association - var postTags = await postTagRepo.ListAsync(new PostTagSpec(request.Id)); - await postTagRepo.DeleteAsync(postTags, ct); + var postTags = await postTagRepo.ListAsync(new PostTagByTagIdSpec(request.Id), ct); + await postTagRepo.DeleteRangeAsync(postTags, ct); // 2. Delte Tag itslef - await tagRepo.DeleteAsync(request.Id, ct); + await tagRepo.DeleteAsync(tag, ct); + logger.LogInformation("Deleted tag: {TagId}", request.Id); return OperationCode.Done; } } \ No newline at end of file diff --git a/src/Moonglade.Core/TagFeature/GetHotTagsQuery.cs b/src/Moonglade.Core/TagFeature/GetHotTagsQuery.cs index eed20e389..203300078 100644 --- a/src/Moonglade.Core/TagFeature/GetHotTagsQuery.cs +++ b/src/Moonglade.Core/TagFeature/GetHotTagsQuery.cs @@ -1,23 +1,18 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; namespace Moonglade.Core.TagFeature; -public record GetHotTagsQuery(int Top) : IRequest>>; +public record GetHotTagsQuery(int Top) : IRequest>; -public class GetHotTagsQueryHandler(IRepository repo) : IRequestHandler>> +public class GetHotTagsQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public async Task>> Handle(GetHotTagsQuery request, CancellationToken ct) + public async Task> Handle(GetHotTagsQuery request, CancellationToken ct) { - if (!await repo.AnyAsync(ct: ct)) return new List>(); + if (!await repo.AnyAsync(ct)) return []; - var spec = new TagSpec(request.Top); - var tags = await repo.SelectAsync(spec, t => - new KeyValuePair(new() - { - Id = t.Id, - DisplayName = t.DisplayName, - NormalizedName = t.NormalizedName - }, t.Posts.Count), ct); + var spec = new HotTagSpec(request.Top); + var tags = await repo.ListAsync(spec, ct); return tags; } diff --git a/src/Moonglade.Core/TagFeature/GetTagCountListQuery.cs b/src/Moonglade.Core/TagFeature/GetTagCountListQuery.cs index ab9b24b6b..d0a47e6b2 100644 --- a/src/Moonglade.Core/TagFeature/GetTagCountListQuery.cs +++ b/src/Moonglade.Core/TagFeature/GetTagCountListQuery.cs @@ -1,15 +1,12 @@ -namespace Moonglade.Core.TagFeature; +using Moonglade.Data; +using Moonglade.Data.Specifications; -public record GetTagCountListQuery : IRequest>>; +namespace Moonglade.Core.TagFeature; -public class GetTagCountListQueryHandler(IRepository repo) : IRequestHandler>> +public record GetTagCountListQuery : IRequest>; + +public class GetTagCountListQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task>> Handle(GetTagCountListQuery request, CancellationToken ct) => - repo.SelectAsync(t => - new KeyValuePair(new() - { - Id = t.Id, - DisplayName = t.DisplayName, - NormalizedName = t.NormalizedName - }, t.Posts.Count), ct); + public Task> Handle(GetTagCountListQuery request, CancellationToken ct) => + repo.ListAsync(new TagCloudSpec(), ct); } \ No newline at end of file diff --git a/src/Moonglade.Core/TagFeature/GetTagNamesQuery.cs b/src/Moonglade.Core/TagFeature/GetTagNamesQuery.cs index 03625c473..7253b84b9 100644 --- a/src/Moonglade.Core/TagFeature/GetTagNamesQuery.cs +++ b/src/Moonglade.Core/TagFeature/GetTagNamesQuery.cs @@ -1,8 +1,12 @@ -namespace Moonglade.Core.TagFeature; +using Moonglade.Data; +using Moonglade.Data.Specifications; -public record GetTagNamesQuery : IRequest>; +namespace Moonglade.Core.TagFeature; -public class GetTagNamesQueryHandler(IRepository repo) : IRequestHandler> +public record GetTagNamesQuery : IRequest>; + +public class GetTagNamesQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(GetTagNamesQuery request, CancellationToken ct) => repo.SelectAsync(t => t.DisplayName, ct); + public Task> Handle(GetTagNamesQuery request, CancellationToken ct) => + repo.ListAsync(new TagDisplayNameNameSpec(), ct); } \ No newline at end of file diff --git a/src/Moonglade.Core/TagFeature/GetTagQuery.cs b/src/Moonglade.Core/TagFeature/GetTagQuery.cs index 3d1d6bab2..03b9df209 100644 --- a/src/Moonglade.Core/TagFeature/GetTagQuery.cs +++ b/src/Moonglade.Core/TagFeature/GetTagQuery.cs @@ -1,11 +1,12 @@ -using Moonglade.Data.Spec; +using Moonglade.Data; +using Moonglade.Data.Specifications; namespace Moonglade.Core.TagFeature; -public record GetTagQuery(string NormalizedName) : IRequest; +public record GetTagQuery(string NormalizedName) : IRequest; -public class GetTagQueryHandler(IRepository repo) : IRequestHandler +public class GetTagQueryHandler(MoongladeRepository repo) : IRequestHandler { - public Task Handle(GetTagQuery request, CancellationToken ct) => - repo.FirstOrDefaultAsync(new TagSpec(request.NormalizedName), Tag.EntitySelector); + public Task Handle(GetTagQuery request, CancellationToken ct) => + repo.FirstOrDefaultAsync(new TagByNormalizedNameSpec(request.NormalizedName), ct); } \ No newline at end of file diff --git a/src/Moonglade.Core/TagFeature/GetTagsQuery.cs b/src/Moonglade.Core/TagFeature/GetTagsQuery.cs index f06152ff7..ab8278cf7 100644 --- a/src/Moonglade.Core/TagFeature/GetTagsQuery.cs +++ b/src/Moonglade.Core/TagFeature/GetTagsQuery.cs @@ -1,8 +1,10 @@ -namespace Moonglade.Core.TagFeature; +using Moonglade.Data; -public record GetTagsQuery : IRequest>; +namespace Moonglade.Core.TagFeature; -public class GetTagsQueryHandler(IRepository repo) : IRequestHandler> +public record GetTagsQuery : IRequest>; + +public class GetTagsQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(GetTagsQuery request, CancellationToken ct) => repo.SelectAsync(Tag.EntitySelector, ct); + public Task> Handle(GetTagsQuery request, CancellationToken ct) => repo.ListAsync(ct); } \ No newline at end of file diff --git a/src/Moonglade.Core/TagFeature/Tag.cs b/src/Moonglade.Core/TagFeature/Tag.cs index 55f1c6d86..574d49555 100644 --- a/src/Moonglade.Core/TagFeature/Tag.cs +++ b/src/Moonglade.Core/TagFeature/Tag.cs @@ -1,64 +1,8 @@ -using System.Linq.Expressions; -using System.Text.RegularExpressions; - -namespace Moonglade.Core.TagFeature; +namespace Moonglade.Core.TagFeature; public class Tag { - public int Id { get; set; } - public string DisplayName { get; set; } public string NormalizedName { get; set; } - - public static readonly Expression> EntitySelector = t => new() - { - Id = t.Id, - NormalizedName = t.NormalizedName, - DisplayName = t.DisplayName - }; - - public static bool ValidateName(string tagDisplayName) - { - if (string.IsNullOrWhiteSpace(tagDisplayName)) return false; - - // Regex performance best practice - // See https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices - - const string pattern = @"^[a-zA-Z 0-9\.\-\+\#\s]*$"; - var isEng = Regex.IsMatch(tagDisplayName, pattern); - if (isEng) return true; - - // https://docs.microsoft.com/en-us/dotnet/standard/base-types/character-classes-in-regular-expressions#supported-named-blocks - const string chsPattern = @"\p{IsCJKUnifiedIdeographs}"; - var isChs = Regex.IsMatch(tagDisplayName, chsPattern); - - return isChs; - } - - public static string NormalizeName(string orgTagName, IDictionary normalizations) - { - var isEnglishName = Regex.IsMatch(orgTagName, @"^[a-zA-Z 0-9\.\-\+\#\s]*$"); - if (isEnglishName) - { - // special case - if (orgTagName.Equals(".net", StringComparison.OrdinalIgnoreCase)) - { - return "dot-net"; - } - - var result = new StringBuilder(orgTagName); - foreach (var (key, value) in normalizations) - { - result.Replace(key, value); - } - return result.ToString().ToLower(); - } - - var bytes = Encoding.Unicode.GetBytes(orgTagName); - var hexArray = bytes.Select(b => $"{b:x2}"); - var hexName = string.Join('-', hexArray); - - return hexName; - } } \ No newline at end of file diff --git a/src/Moonglade.Core/TagFeature/UpdateTagCommand.cs b/src/Moonglade.Core/TagFeature/UpdateTagCommand.cs index c6fba81f7..2495fea28 100644 --- a/src/Moonglade.Core/TagFeature/UpdateTagCommand.cs +++ b/src/Moonglade.Core/TagFeature/UpdateTagCommand.cs @@ -1,22 +1,26 @@ -using Moonglade.Data; +using Microsoft.Extensions.Logging; +using Moonglade.Data; using Moonglade.Utils; namespace Moonglade.Core.TagFeature; public record UpdateTagCommand(int Id, string Name) : IRequest; -public class UpdateTagCommandHandler(IRepository repo) : IRequestHandler +public class UpdateTagCommandHandler( + MoongladeRepository repo, + ILogger logger) : IRequestHandler { public async Task Handle(UpdateTagCommand request, CancellationToken ct) { var (id, name) = request; - var tag = await repo.GetAsync(id, ct); + var tag = await repo.GetByIdAsync(id, ct); if (null == tag) return OperationCode.ObjectNotFound; tag.DisplayName = name; - tag.NormalizedName = Tag.NormalizeName(name, Helper.TagNormalizationDictionary); + tag.NormalizedName = Helper.NormalizeName(name, Helper.TagNormalizationDictionary); await repo.UpdateAsync(tag, ct); + logger.LogInformation("Updated tag: {TagId}", request.Id); return OperationCode.Done; } } \ No newline at end of file diff --git a/src/Moonglade.Data.MySql/Configurations/LocalAccountConfiguration.cs b/src/Moonglade.Data.MySql/Configurations/LocalAccountConfiguration.cs deleted file mode 100644 index a2141a57e..000000000 --- a/src/Moonglade.Data.MySql/Configurations/LocalAccountConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Moonglade.Data.Entities; - -namespace Moonglade.Data.MySql.Configurations; - - -internal class LocalAccountConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.Property(e => e.Id).ValueGeneratedNever(); - builder.Property(e => e.Username).HasMaxLength(32); - builder.Property(e => e.PasswordHash).HasMaxLength(64); - builder.Property(e => e.LastLoginIp).HasMaxLength(64); - builder.Property(e => e.CreateTimeUtc).HasColumnType("datetime"); - builder.Property(e => e.LastLoginTimeUtc).HasColumnType("datetime"); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data.MySql/Infrastructure/MySqlDbContextRepository.cs b/src/Moonglade.Data.MySql/Infrastructure/MySqlDbContextRepository.cs index 6e8e1d969..14f83a157 100644 --- a/src/Moonglade.Data.MySql/Infrastructure/MySqlDbContextRepository.cs +++ b/src/Moonglade.Data.MySql/Infrastructure/MySqlDbContextRepository.cs @@ -1,7 +1,5 @@ -using Moonglade.Data.Infrastructure; +namespace Moonglade.Data.MySql.Infrastructure; -namespace Moonglade.Data.MySql.Infrastructure; - -public class MySqlDbContextRepository(MySqlBlogDbContext dbContext) : DbContextRepository(dbContext) +public class MySqlDbContextRepository(MySqlBlogDbContext dbContext) : MoongladeRepository(dbContext) where T : class; \ No newline at end of file diff --git a/src/Moonglade.Data.MySql/MySqlBlogDbContext.cs b/src/Moonglade.Data.MySql/MySqlBlogDbContext.cs index 66bb4ab34..ee6c135f3 100644 --- a/src/Moonglade.Data.MySql/MySqlBlogDbContext.cs +++ b/src/Moonglade.Data.MySql/MySqlBlogDbContext.cs @@ -21,7 +21,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new CommentReplyConfiguration()); modelBuilder.ApplyConfiguration(new PostConfiguration()); modelBuilder.ApplyConfiguration(new PostCategoryConfiguration()); - modelBuilder.ApplyConfiguration(new LocalAccountConfiguration()); modelBuilder.ApplyConfiguration(new PingbackConfiguration()); modelBuilder.ApplyConfiguration(new BlogThemeConfiguration()); modelBuilder.ApplyConfiguration(new BlogAssetConfiguration()); diff --git a/src/Moonglade.Data.MySql/ServiceCollectionExtensions.cs b/src/Moonglade.Data.MySql/ServiceCollectionExtensions.cs index fe165f700..8e5d176ff 100644 --- a/src/Moonglade.Data.MySql/ServiceCollectionExtensions.cs +++ b/src/Moonglade.Data.MySql/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Moonglade.Data.Infrastructure; using Moonglade.Data.MySql.Infrastructure; namespace Moonglade.Data.MySql; @@ -10,7 +9,7 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddMySqlStorage(this IServiceCollection services, string connectionString) { - services.AddScoped(typeof(IRepository<>), typeof(MySqlDbContextRepository<>)); + services.AddScoped(typeof(MoongladeRepository<>), typeof(MySqlDbContextRepository<>)); services.AddDbContext(optionsAction => optionsAction.UseLazyLoadingProxies() .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder => diff --git a/src/Moonglade.Data.PostgreSql/Configurations/LocalAccountConfiguration.cs b/src/Moonglade.Data.PostgreSql/Configurations/LocalAccountConfiguration.cs deleted file mode 100644 index c691cb8a7..000000000 --- a/src/Moonglade.Data.PostgreSql/Configurations/LocalAccountConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Moonglade.Data.Entities; - -namespace Moonglade.Data.PostgreSql.Configurations; - - -internal class LocalAccountConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.Property(e => e.Id).ValueGeneratedNever(); - builder.Property(e => e.Username).HasMaxLength(32); - builder.Property(e => e.PasswordHash).HasMaxLength(64); - builder.Property(e => e.LastLoginIp).HasMaxLength(64); - builder.Property(e => e.CreateTimeUtc).HasColumnType("timestamp"); - builder.Property(e => e.LastLoginTimeUtc).HasColumnType("timestamp"); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data.PostgreSql/Configurations/LoginHistoryConfiguration.cs b/src/Moonglade.Data.PostgreSql/Configurations/LoginHistoryConfiguration.cs new file mode 100644 index 000000000..3781f1224 --- /dev/null +++ b/src/Moonglade.Data.PostgreSql/Configurations/LoginHistoryConfiguration.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Moonglade.Data.Entities; + +namespace Moonglade.Data.PostgreSql.Configurations; + +internal class LoginHistoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(e => e.Id).UseIdentityColumn(); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data.PostgreSql/Infrastructure/PostgreSqlDbContextRepository.cs b/src/Moonglade.Data.PostgreSql/Infrastructure/PostgreSqlDbContextRepository.cs index 0f0950642..cea43e290 100644 --- a/src/Moonglade.Data.PostgreSql/Infrastructure/PostgreSqlDbContextRepository.cs +++ b/src/Moonglade.Data.PostgreSql/Infrastructure/PostgreSqlDbContextRepository.cs @@ -1,6 +1,4 @@ -using Moonglade.Data.Infrastructure; +namespace Moonglade.Data.PostgreSql.Infrastructure; -namespace Moonglade.Data.PostgreSql.Infrastructure; - -public class PostgreSqlDbContextRepository(PostgreSqlBlogDbContext dbContext) : DbContextRepository(dbContext) +public class PostgreSqlDbContextRepository(PostgreSqlBlogDbContext dbContext) : MoongladeRepository(dbContext) where T : class; diff --git a/src/Moonglade.Data.PostgreSql/PostgreSqlBlogDbContext.cs b/src/Moonglade.Data.PostgreSql/PostgreSqlBlogDbContext.cs index 4889ae458..44815882f 100644 --- a/src/Moonglade.Data.PostgreSql/PostgreSqlBlogDbContext.cs +++ b/src/Moonglade.Data.PostgreSql/PostgreSqlBlogDbContext.cs @@ -19,7 +19,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new CommentReplyConfiguration()); modelBuilder.ApplyConfiguration(new PostConfiguration()); modelBuilder.ApplyConfiguration(new PostCategoryConfiguration()); - modelBuilder.ApplyConfiguration(new LocalAccountConfiguration()); + modelBuilder.ApplyConfiguration(new LoginHistoryConfiguration()); modelBuilder.ApplyConfiguration(new PingbackConfiguration()); modelBuilder.ApplyConfiguration(new BlogThemeConfiguration()); modelBuilder.ApplyConfiguration(new BlogAssetConfiguration()); diff --git a/src/Moonglade.Data.PostgreSql/ServiceCollectionExtensions.cs b/src/Moonglade.Data.PostgreSql/ServiceCollectionExtensions.cs index 823357558..0c3d54364 100644 --- a/src/Moonglade.Data.PostgreSql/ServiceCollectionExtensions.cs +++ b/src/Moonglade.Data.PostgreSql/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Moonglade.Data.Infrastructure; using Moonglade.Data.PostgreSql.Infrastructure; namespace Moonglade.Data.PostgreSql; @@ -9,7 +8,7 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddPostgreSqlStorage(this IServiceCollection services, string connectionString) { - services.AddScoped(typeof(IRepository<>), typeof(PostgreSqlDbContextRepository<>)); + services.AddScoped(typeof(MoongladeRepository<>), typeof(PostgreSqlDbContextRepository<>)); AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); services.AddDbContext(optionsAction => optionsAction diff --git a/src/Moonglade.Data.SqlServer/Configurations/LocalAccountConfiguration.cs b/src/Moonglade.Data.SqlServer/Configurations/LocalAccountConfiguration.cs deleted file mode 100644 index 4392de7e9..000000000 --- a/src/Moonglade.Data.SqlServer/Configurations/LocalAccountConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Moonglade.Data.Entities; - -namespace Moonglade.Data.SqlServer.Configurations; - - -internal class LocalAccountConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.Property(e => e.Id).ValueGeneratedNever(); - builder.Property(e => e.Username).HasMaxLength(32); - builder.Property(e => e.PasswordHash).HasMaxLength(64); - builder.Property(e => e.LastLoginIp).HasMaxLength(64); - builder.Property(e => e.CreateTimeUtc).HasColumnType("datetime"); - builder.Property(e => e.LastLoginTimeUtc).HasColumnType("datetime"); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data.SqlServer/Configurations/LoginHistoryConfiguration.cs b/src/Moonglade.Data.SqlServer/Configurations/LoginHistoryConfiguration.cs new file mode 100644 index 000000000..9620e0617 --- /dev/null +++ b/src/Moonglade.Data.SqlServer/Configurations/LoginHistoryConfiguration.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Moonglade.Data.Entities; + +namespace Moonglade.Data.SqlServer.Configurations; + +internal class LoginHistoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(e => e.Id).UseIdentityColumn(1, 1); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data.SqlServer/Infrastructure/SqlServerDbContextRepository.cs b/src/Moonglade.Data.SqlServer/Infrastructure/SqlServerDbContextRepository.cs index a08e3e011..7de361ea5 100644 --- a/src/Moonglade.Data.SqlServer/Infrastructure/SqlServerDbContextRepository.cs +++ b/src/Moonglade.Data.SqlServer/Infrastructure/SqlServerDbContextRepository.cs @@ -1,7 +1,5 @@ -using Moonglade.Data.Infrastructure; +namespace Moonglade.Data.SqlServer.Infrastructure; -namespace Moonglade.Data.SqlServer.Infrastructure; - -public class SqlServerDbContextRepository(SqlServerBlogDbContext dbContext) : DbContextRepository(dbContext) +public class SqlServerDbContextRepository(SqlServerBlogDbContext dbContext) : MoongladeRepository(dbContext) where T : class; \ No newline at end of file diff --git a/src/Moonglade.Data.SqlServer/Moonglade.Data.SqlServer.csproj b/src/Moonglade.Data.SqlServer/Moonglade.Data.SqlServer.csproj index 048121024..4e2a69f1a 100644 --- a/src/Moonglade.Data.SqlServer/Moonglade.Data.SqlServer.csproj +++ b/src/Moonglade.Data.SqlServer/Moonglade.Data.SqlServer.csproj @@ -10,7 +10,7 @@ enable - + diff --git a/src/Moonglade.Data.SqlServer/ServiceCollectionExtensions.cs b/src/Moonglade.Data.SqlServer/ServiceCollectionExtensions.cs index 1484ec91f..9a53cbfa7 100644 --- a/src/Moonglade.Data.SqlServer/ServiceCollectionExtensions.cs +++ b/src/Moonglade.Data.SqlServer/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Moonglade.Data.Infrastructure; using Moonglade.Data.SqlServer.Infrastructure; namespace Moonglade.Data.SqlServer; @@ -10,7 +9,7 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddSqlServerStorage(this IServiceCollection services, string connectionString) { - services.AddScoped(typeof(IRepository<>), typeof(SqlServerDbContextRepository<>)); + services.AddScoped(typeof(MoongladeRepository<>), typeof(SqlServerDbContextRepository<>)); services.AddDbContext(options => options.UseLazyLoadingProxies() diff --git a/src/Moonglade.Data.SqlServer/SqlServerBlogDbContext.cs b/src/Moonglade.Data.SqlServer/SqlServerBlogDbContext.cs index 66c70be71..6c39cead6 100644 --- a/src/Moonglade.Data.SqlServer/SqlServerBlogDbContext.cs +++ b/src/Moonglade.Data.SqlServer/SqlServerBlogDbContext.cs @@ -21,7 +21,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new CommentReplyConfiguration()); modelBuilder.ApplyConfiguration(new PostConfiguration()); modelBuilder.ApplyConfiguration(new PostCategoryConfiguration()); - modelBuilder.ApplyConfiguration(new LocalAccountConfiguration()); + modelBuilder.ApplyConfiguration(new LoginHistoryConfiguration()); modelBuilder.ApplyConfiguration(new PingbackConfiguration()); modelBuilder.ApplyConfiguration(new BlogThemeConfiguration()); modelBuilder.ApplyConfiguration(new BlogAssetConfiguration()); diff --git a/src/Moonglade.Data/BlogDbContext.cs b/src/Moonglade.Data/BlogDbContext.cs index 84abcb721..28a6bbac7 100644 --- a/src/Moonglade.Data/BlogDbContext.cs +++ b/src/Moonglade.Data/BlogDbContext.cs @@ -22,7 +22,7 @@ public BlogDbContext(DbContextOptions options) public virtual DbSet Tag { get; set; } public virtual DbSet FriendLink { get; set; } public virtual DbSet CustomPage { get; set; } - public virtual DbSet LocalAccount { get; set; } + public virtual DbSet LoginHistory { get; set; } public virtual DbSet Pingback { get; set; } public virtual DbSet BlogTheme { get; set; } public virtual DbSet StyleSheet { get; set; } @@ -33,9 +33,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { // base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new CategoryConfiguration()); - modelBuilder.ApplyConfiguration(new TagConfiguration()); modelBuilder.ApplyConfiguration(new FriendLinkConfiguration()); - modelBuilder.ApplyConfiguration(new BlogConfigurationConfiguration()); modelBuilder .Entity() @@ -69,7 +67,8 @@ public static async Task ClearAllData(this BlogDbContext context) context.BlogConfiguration.RemoveRange(); context.BlogAsset.RemoveRange(); context.BlogTheme.RemoveRange(); - context.LocalAccount.RemoveRange(); + context.StyleSheet.RemoveRange(); + context.LoginHistory.RemoveRange(); await context.SaveChangesAsync(); } diff --git a/src/Moonglade.Data/Entities/BlogConfigurationEntity.cs b/src/Moonglade.Data/Entities/BlogConfigurationEntity.cs index 02f0ea4c2..d7ad5000b 100644 --- a/src/Moonglade.Data/Entities/BlogConfigurationEntity.cs +++ b/src/Moonglade.Data/Entities/BlogConfigurationEntity.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System.ComponentModel.DataAnnotations; namespace Moonglade.Data.Entities; @@ -6,18 +6,10 @@ public class BlogConfigurationEntity { public int Id { get; set; } + [MaxLength(64)] public string CfgKey { get; set; } public string CfgValue { get; set; } public DateTime? LastModifiedTimeUtc { get; set; } -} - - -internal class BlogConfigurationConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.Property(e => e.CfgKey).HasMaxLength(64); - } } \ No newline at end of file diff --git a/src/Moonglade.Data/Entities/CategoryEntity.cs b/src/Moonglade.Data/Entities/CategoryEntity.cs index f4de9ba7c..ff15e8132 100644 --- a/src/Moonglade.Data/Entities/CategoryEntity.cs +++ b/src/Moonglade.Data/Entities/CategoryEntity.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System.Text.Json.Serialization; namespace Moonglade.Data.Entities; @@ -10,10 +11,11 @@ public CategoryEntity() } public Guid Id { get; set; } - public string RouteName { get; set; } + public string Slug { get; set; } public string DisplayName { get; set; } public string Note { get; set; } + [JsonIgnore] public virtual ICollection PostCategory { get; set; } } @@ -24,6 +26,6 @@ public void Configure(EntityTypeBuilder builder) builder.Property(e => e.Id).ValueGeneratedNever(); builder.Property(e => e.DisplayName).HasMaxLength(64); builder.Property(e => e.Note).HasMaxLength(128); - builder.Property(e => e.RouteName).HasMaxLength(64); + builder.Property(e => e.Slug).HasMaxLength(64); } } \ No newline at end of file diff --git a/src/Moonglade.Data/Entities/LocalAccountEntity.cs b/src/Moonglade.Data/Entities/LocalAccountEntity.cs deleted file mode 100644 index 09161d667..000000000 --- a/src/Moonglade.Data/Entities/LocalAccountEntity.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Moonglade.Data.Entities; - -public class LocalAccountEntity -{ - public Guid Id { get; set; } - public string Username { get; set; } - public string PasswordSalt { get; set; } - public string PasswordHash { get; set; } - public DateTime? LastLoginTimeUtc { get; set; } - public string LastLoginIp { get; set; } - public DateTime CreateTimeUtc { get; set; } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Entities/LoginHistoryEntity.cs b/src/Moonglade.Data/Entities/LoginHistoryEntity.cs new file mode 100644 index 000000000..5c19d81c0 --- /dev/null +++ b/src/Moonglade.Data/Entities/LoginHistoryEntity.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Moonglade.Data.Entities; + +public class LoginHistoryEntity +{ + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + public DateTime LoginTimeUtc { get; set; } + + [MaxLength(64)] + public string LoginIp { get; set; } + + [MaxLength(128)] + public string LoginUserAgent { get; set; } + + [MaxLength(128)] + public string DeviceFingerprint { get; set; } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Entities/PostCategoryEntity.cs b/src/Moonglade.Data/Entities/PostCategoryEntity.cs index a415d6503..037a5ecb0 100644 --- a/src/Moonglade.Data/Entities/PostCategoryEntity.cs +++ b/src/Moonglade.Data/Entities/PostCategoryEntity.cs @@ -1,10 +1,15 @@ -namespace Moonglade.Data.Entities; +using System.Text.Json.Serialization; + +namespace Moonglade.Data.Entities; public class PostCategoryEntity { public Guid PostId { get; set; } public Guid CategoryId { get; set; } + [JsonIgnore] public virtual CategoryEntity Category { get; set; } + + [JsonIgnore] public virtual PostEntity Post { get; set; } } \ No newline at end of file diff --git a/src/Moonglade.Data/Entities/TagEntity.cs b/src/Moonglade.Data/Entities/TagEntity.cs index 3ca803647..b60241091 100644 --- a/src/Moonglade.Data/Entities/TagEntity.cs +++ b/src/Moonglade.Data/Entities/TagEntity.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace Moonglade.Data.Entities; @@ -10,17 +11,13 @@ public TagEntity() } public int Id { get; set; } + + [MaxLength(32)] public string DisplayName { get; set; } + + [MaxLength(32)] public string NormalizedName { get; set; } + [JsonIgnore] public virtual ICollection Posts { get; set; } -} - -internal class TagConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.Property(e => e.DisplayName).HasMaxLength(32); - builder.Property(e => e.NormalizedName).HasMaxLength(32); - } } \ No newline at end of file diff --git a/src/Moonglade.Data/Exporting/ExportFormat.cs b/src/Moonglade.Data/Exporting/ExportFormat.cs deleted file mode 100644 index 48e4301e4..000000000 --- a/src/Moonglade.Data/Exporting/ExportFormat.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Moonglade.Data.Exporting; - -public enum ExportFormat -{ - ZippedJsonFiles -} \ No newline at end of file diff --git a/src/Moonglade.Data/Exporting/ExportManager.cs b/src/Moonglade.Data/Exporting/ExportManager.cs deleted file mode 100644 index b75da30af..000000000 --- a/src/Moonglade.Data/Exporting/ExportManager.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Moonglade.Data.Exporting; - -public class ExportManager -{ - public static readonly string DataDir = Path.GetTempPath(); - - public static string CreateExportDirectory(string directory, string subDirName) - { - if (directory is null) return null; - - var path = Path.Join(directory, "export", subDirName); - if (Directory.Exists(path)) - { - Directory.Delete(path); - } - - Directory.CreateDirectory(path); - return path; - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Exporting/ExportPageDataCommand.cs b/src/Moonglade.Data/Exporting/ExportPageDataCommand.cs index 9e278e6cc..9478fa265 100644 --- a/src/Moonglade.Data/Exporting/ExportPageDataCommand.cs +++ b/src/Moonglade.Data/Exporting/ExportPageDataCommand.cs @@ -1,29 +1,15 @@ using MediatR; using Moonglade.Data.Entities; -using Moonglade.Data.Exporting.Exporters; -using Moonglade.Data.Infrastructure; namespace Moonglade.Data.Exporting; public record ExportPageDataCommand : IRequest; -public class ExportPageDataCommandHandler(IRepository repo) : IRequestHandler +public class ExportPageDataCommandHandler(MoongladeRepository repo) : IRequestHandler { public Task Handle(ExportPageDataCommand request, CancellationToken ct) { - var pgExp = new ZippedJsonExporter(repo, "moonglade-pages", ExportManager.DataDir); - return pgExp.ExportData(p => new - { - p.Id, - p.Title, - p.Slug, - p.MetaDescription, - p.HtmlContent, - p.CssId, - p.HideSidebar, - p.IsPublished, - p.CreateTimeUtc, - p.UpdateTimeUtc - }, ct); + var pgExp = new ZippedJsonExporter(repo, "moonglade-pages", Path.GetTempPath()); + return pgExp.ExportData(p => p, ct); } } \ No newline at end of file diff --git a/src/Moonglade.Data/Exporting/ExportPostDataCommand.cs b/src/Moonglade.Data/Exporting/ExportPostDataCommand.cs index 25fcb2756..533412295 100644 --- a/src/Moonglade.Data/Exporting/ExportPostDataCommand.cs +++ b/src/Moonglade.Data/Exporting/ExportPostDataCommand.cs @@ -1,17 +1,15 @@ using MediatR; using Moonglade.Data.Entities; -using Moonglade.Data.Exporting.Exporters; -using Moonglade.Data.Infrastructure; namespace Moonglade.Data.Exporting; public record ExportPostDataCommand : IRequest; -public class ExportPostDataCommandHandler(IRepository repo) : IRequestHandler +public class ExportPostDataCommandHandler(MoongladeRepository repo) : IRequestHandler { public Task Handle(ExportPostDataCommand request, CancellationToken ct) { - var poExp = new ZippedJsonExporter(repo, "moonglade-posts", ExportManager.DataDir); + var poExp = new ZippedJsonExporter(repo, "moonglade-posts", Path.GetTempPath()); var poExportData = poExp.ExportData(p => new { p.Title, diff --git a/src/Moonglade.Data/Exporting/ExportResult.cs b/src/Moonglade.Data/Exporting/ExportResult.cs index f86c80624..6b74717f6 100644 --- a/src/Moonglade.Data/Exporting/ExportResult.cs +++ b/src/Moonglade.Data/Exporting/ExportResult.cs @@ -1,22 +1,6 @@ namespace Moonglade.Data.Exporting; -public class ExportResult +public record ExportResult { - public ExportFormat ExportFormat { get; set; } - public string FilePath { get; set; } - - public byte[] Content { get; set; } - - public string ContentType - { - get - { - return ExportFormat switch - { - ExportFormat.ZippedJsonFiles => "application/zip", - _ => string.Empty - }; - } - } } \ No newline at end of file diff --git a/src/Moonglade.Data/Exporting/Exporters/IExporter.cs b/src/Moonglade.Data/Exporting/Exporters/IExporter.cs deleted file mode 100644 index ee2a916f5..000000000 --- a/src/Moonglade.Data/Exporting/Exporters/IExporter.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Linq.Expressions; - -namespace Moonglade.Data.Exporting.Exporters; - -public interface IExporter -{ - Task ExportData(Expression> selector, CancellationToken ct); -} \ No newline at end of file diff --git a/src/Moonglade.Data/Exporting/Exporters/MoongladeJsonSerializerOptions.cs b/src/Moonglade.Data/Exporting/MoongladeJsonSerializerOptions.cs similarity index 90% rename from src/Moonglade.Data/Exporting/Exporters/MoongladeJsonSerializerOptions.cs rename to src/Moonglade.Data/Exporting/MoongladeJsonSerializerOptions.cs index 558ec0ecd..8392cc318 100644 --- a/src/Moonglade.Data/Exporting/Exporters/MoongladeJsonSerializerOptions.cs +++ b/src/Moonglade.Data/Exporting/MoongladeJsonSerializerOptions.cs @@ -2,7 +2,7 @@ using System.Text.Json; using System.Text.Unicode; -namespace Moonglade.Data.Exporting.Exporters; +namespace Moonglade.Data.Exporting; public static class MoongladeJsonSerializerOptions { @@ -21,6 +21,7 @@ public static class MoongladeJsonSerializerOptions UnicodeRanges.CjkSymbolsandPunctuation, UnicodeRanges.HalfwidthandFullwidthForms), PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }; } \ No newline at end of file diff --git a/src/Moonglade.Data/Exporting/Exporters/ZippedJsonExporter.cs b/src/Moonglade.Data/Exporting/ZippedJsonExporter.cs similarity index 67% rename from src/Moonglade.Data/Exporting/Exporters/ZippedJsonExporter.cs rename to src/Moonglade.Data/Exporting/ZippedJsonExporter.cs index 9abe8d8ed..7fa65e114 100644 --- a/src/Moonglade.Data/Exporting/Exporters/ZippedJsonExporter.cs +++ b/src/Moonglade.Data/Exporting/ZippedJsonExporter.cs @@ -1,13 +1,11 @@ -using Moonglade.Data.Infrastructure; -using System.IO.Compression; +using System.IO.Compression; using System.Linq.Expressions; using System.Text; using System.Text.Json; -namespace Moonglade.Data.Exporting.Exporters; +namespace Moonglade.Data.Exporting; -public class ZippedJsonExporter(IRepository repository, string fileNamePrefix, string directory) - : IExporter +public class ZippedJsonExporter(MoongladeRepository repository, string fileNamePrefix, string directory) where T : class { public async Task ExportData(Expression> selector, CancellationToken ct) @@ -20,7 +18,7 @@ public async Task ExportData(Expression> private async Task ToZippedJsonResult(IEnumerable list, CancellationToken ct) { var tempId = Guid.NewGuid().ToString(); - string exportDirectory = ExportManager.CreateExportDirectory(directory, tempId); + string exportDirectory = CreateExportDirectory(directory, tempId); foreach (var item in list) { var json = JsonSerializer.Serialize(item, MoongladeJsonSerializerOptions.Default); @@ -32,7 +30,6 @@ private async Task ToZippedJsonResult(IEnumerable list, Ca return new() { - ExportFormat = ExportFormat.ZippedJsonFiles, FilePath = distPath }; } @@ -42,4 +39,18 @@ private static async Task SaveJsonToDirectory(string json, string directory, str var path = Path.Join(directory, filename); await File.WriteAllTextAsync(path, json, Encoding.UTF8, ct); } + + private static string CreateExportDirectory(string directory, string subDirName) + { + if (directory is null) return null; + + var path = Path.Join(directory, "export", subDirName); + if (Directory.Exists(path)) + { + Directory.Delete(path); + } + + Directory.CreateDirectory(path); + return path; + } } \ No newline at end of file diff --git a/src/Moonglade.Data/ExpressionExtentions.cs b/src/Moonglade.Data/ExpressionExtentions.cs new file mode 100644 index 000000000..f2f14eac6 --- /dev/null +++ b/src/Moonglade.Data/ExpressionExtentions.cs @@ -0,0 +1,32 @@ +using System.Linq.Expressions; + +namespace Moonglade.Data; + +// https://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct-bool +public static class ExpressionExtentions +{ + public static Expression> AndAlso( + this Expression> expr1, + Expression> expr2) + { + var parameter = Expression.Parameter(typeof(T)); + + var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter); + var left = leftVisitor.Visit(expr1.Body); + + var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter); + var right = rightVisitor.Visit(expr2.Body); + + return Expression.Lambda>( + Expression.AndAlso(left ?? throw new InvalidOperationException(), + right ?? throw new InvalidOperationException()), parameter); + } + + private class ReplaceExpressionVisitor(Expression oldValue, Expression newValue) : ExpressionVisitor + { + public override Expression Visit(Expression node) + { + return node == oldValue ? newValue : base.Visit(node); + } + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Infrastructure/BaseSpecification.cs b/src/Moonglade.Data/Infrastructure/BaseSpecification.cs deleted file mode 100644 index 6033b6c13..000000000 --- a/src/Moonglade.Data/Infrastructure/BaseSpecification.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Microsoft.EntityFrameworkCore.Query; -using System.Linq.Expressions; - -namespace Moonglade.Data.Infrastructure; - -public abstract class BaseSpecification : ISpecification -{ - protected BaseSpecification() - { - } - - protected BaseSpecification(Expression> criteria) => Criteria = criteria; - - public Expression> Criteria { get; private set; } - public Func, IIncludableQueryable> Include { get; private set; } - public Expression> OrderBy { get; private set; } - public Expression> OrderByDescending { get; private set; } - - public int Take { get; private set; } - public int Skip { get; private set; } - public bool IsPagingEnabled { get; private set; } - - public void AddCriteria(Expression> criteria) - { - Criteria = Criteria is not null ? Criteria.AndAlso(criteria) : criteria; - } - - protected virtual void AddInclude(Func, IIncludableQueryable> expression) => Include = expression; - - protected virtual void ApplyPaging(int skip, int take) - { - Skip = skip; - Take = take; - IsPagingEnabled = true; - } - - protected virtual void ApplyOrderBy(Expression> expression) => OrderBy = expression; - - protected virtual void ApplyOrderByDescending(Expression> expression) => OrderByDescending = expression; -} - -// https://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct-bool -public static class ExpressionExtentions -{ - public static Expression> AndAlso( - this Expression> expr1, - Expression> expr2) - { - var parameter = Expression.Parameter(typeof(T)); - - var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter); - var left = leftVisitor.Visit(expr1.Body); - - var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter); - var right = rightVisitor.Visit(expr2.Body); - - return Expression.Lambda>( - Expression.AndAlso(left ?? throw new InvalidOperationException(), - right ?? throw new InvalidOperationException()), parameter); - } - - private class ReplaceExpressionVisitor(Expression oldValue, Expression newValue) : ExpressionVisitor - { - public override Expression Visit(Expression node) - { - return node == oldValue ? newValue : base.Visit(node); - } - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Infrastructure/DbContextRepository.cs b/src/Moonglade.Data/Infrastructure/DbContextRepository.cs deleted file mode 100644 index 1ad73cb44..000000000 --- a/src/Moonglade.Data/Infrastructure/DbContextRepository.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Linq.Expressions; - -namespace Moonglade.Data.Infrastructure; - -public abstract class DbContextRepository(DbContext ctx) : IRepository - where T : class -{ - protected readonly DbContext DbContext = ctx; - - public Task Clear(CancellationToken ct = default) - { - DbContext.RemoveRange(DbContext.Set()); - return DbContext.SaveChangesAsync(ct); - } - - public Task GetAsync(Expression> condition) => - DbContext.Set().FirstOrDefaultAsync(condition); - - public virtual ValueTask GetAsync(object key, CancellationToken ct = default) => - DbContext.Set().FindAsync(keyValues: new[] { key }, cancellationToken: ct); - - public async Task> ListAsync(CancellationToken ct = default) => - await DbContext.Set().AsNoTracking().ToListAsync(cancellationToken: ct); - - public async Task> ListAsync(ISpecification spec) => - await ApplySpecification(spec).AsNoTracking().ToListAsync(); - - public IQueryable AsQueryable() => DbContext.Set(); - - public async Task DeleteAsync(T entity, CancellationToken ct = default) - { - DbContext.Set().Remove(entity); - await DbContext.SaveChangesAsync(ct); - } - - public Task DeleteAsync(IEnumerable entities, CancellationToken ct = default) - { - DbContext.Set().RemoveRange(entities); - return DbContext.SaveChangesAsync(ct); - } - - public async Task DeleteAsync(object key, CancellationToken ct = default) - { - var entity = await GetAsync(key, ct); - if (entity is not null) await DeleteAsync(entity, ct); - } - - public Task CountAsync(Expression> condition, CancellationToken ct = default) => - DbContext.Set().CountAsync(condition, ct); - - public Task CountAsync(ISpecification spec = null, CancellationToken ct = default) => - null != spec ? - ApplySpecification(spec).CountAsync(cancellationToken: ct) : - DbContext.Set().CountAsync(cancellationToken: ct); - - public Task AnyAsync(ISpecification spec, CancellationToken ct = default) => - ApplySpecification(spec).AnyAsync(cancellationToken: ct); - - public Task AnyAsync(Expression> condition = null, CancellationToken ct = default) => - null != condition ? - DbContext.Set().AnyAsync(condition, cancellationToken: ct) : - DbContext.Set().AnyAsync(cancellationToken: ct); - - public async Task> SelectAsync(Expression> selector, CancellationToken ct = default) => - await DbContext.Set().AsNoTracking().Select(selector).ToListAsync(cancellationToken: ct); - - public async Task> SelectAsync( - ISpecification spec, Expression> selector, CancellationToken ct = default) => - await ApplySpecification(spec).AsNoTracking().Select(selector).ToListAsync(ct); - - public Task FirstOrDefaultAsync( - ISpecification spec, Expression> selector) => - ApplySpecification(spec).AsNoTracking().Select(selector).FirstOrDefaultAsync(); - - public async Task> SelectAsync( - Expression> groupExpression, - Expression, TResult>> selector, - ISpecification spec = null) => - null != spec ? - await ApplySpecification(spec).AsNoTracking().GroupBy(groupExpression).Select(selector).ToListAsync() : - await DbContext.Set().AsNoTracking().GroupBy(groupExpression).Select(selector).ToListAsync(); - - public async Task AddAsync(T entity, CancellationToken ct = default) - { - await DbContext.Set().AddAsync(entity, ct); - await DbContext.SaveChangesAsync(ct); - - return entity; - } - - public async Task UpdateAsync(T entity, CancellationToken ct = default) - { - DbContext.Entry(entity).State = EntityState.Modified; - await DbContext.SaveChangesAsync(ct); - } - - private IQueryable ApplySpecification(ISpecification spec) => - SpecificationEvaluator.GetQuery(DbContext.Set().AsQueryable(), spec); -} \ No newline at end of file diff --git a/src/Moonglade.Data/Infrastructure/IRepository.cs b/src/Moonglade.Data/Infrastructure/IRepository.cs deleted file mode 100644 index aa5cfac8b..000000000 --- a/src/Moonglade.Data/Infrastructure/IRepository.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Linq.Expressions; - -namespace Moonglade.Data.Infrastructure; - -public interface IRepository where T : class -{ - Task Clear(CancellationToken ct = default); - - ValueTask GetAsync(object key, CancellationToken ct = default); - - Task GetAsync(Expression> condition); - - Task> ListAsync(CancellationToken ct = default); - - Task> ListAsync(ISpecification spec); - - IQueryable AsQueryable(); - - Task DeleteAsync(T entity, CancellationToken ct = default); - - Task DeleteAsync(IEnumerable entities, CancellationToken ct = default); - - Task DeleteAsync(object key, CancellationToken ct = default); - - Task CountAsync(Expression> condition, CancellationToken ct = default); - - Task CountAsync(ISpecification spec = null, CancellationToken ct = default); - - Task AnyAsync(ISpecification spec, CancellationToken ct = default); - - Task AnyAsync(Expression> condition = null, CancellationToken ct = default); - - Task> SelectAsync(Expression> selector, CancellationToken ct = default); - - Task> SelectAsync(ISpecification spec, Expression> selector, CancellationToken ct = default); - - Task FirstOrDefaultAsync(ISpecification spec, Expression> selector); - - Task> SelectAsync( - Expression> groupExpression, - Expression, TResult>> selector, - ISpecification spec = null); - - Task AddAsync(T entity, CancellationToken ct = default); - - Task UpdateAsync(T entity, CancellationToken ct = default); -} \ No newline at end of file diff --git a/src/Moonglade.Data/Infrastructure/ISpecification.cs b/src/Moonglade.Data/Infrastructure/ISpecification.cs deleted file mode 100644 index c9139d286..000000000 --- a/src/Moonglade.Data/Infrastructure/ISpecification.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.EntityFrameworkCore.Query; -using System.Linq.Expressions; - -namespace Moonglade.Data.Infrastructure; - -public interface ISpecification -{ - Expression> Criteria { get; } - Func, IIncludableQueryable> Include { get; } - Expression> OrderBy { get; } - Expression> OrderByDescending { get; } - - int Take { get; } - int Skip { get; } - bool IsPagingEnabled { get; } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Infrastructure/SpecificationEvaluator.cs b/src/Moonglade.Data/Infrastructure/SpecificationEvaluator.cs deleted file mode 100644 index 1031022ad..000000000 --- a/src/Moonglade.Data/Infrastructure/SpecificationEvaluator.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace Moonglade.Data.Infrastructure; - -public class SpecificationEvaluator where T : class -{ - public static IQueryable GetQuery(IQueryable inputQuery, ISpecification specification) - { - var query = inputQuery; - - // modify the IQueryable using the specification's criteria expression - if (specification.Criteria is not null) - { - query = query.Where(specification.Criteria); - } - - //// Includes all expression-based includes - //query = specification.Includes.Aggregate(query, - // (current, include) => current.Include(include)); - - //// Include any string-based include statements - //query = specification.IncludeStrings.Aggregate(query, - // (current, include) => current.Include(include)); - - if (specification.Include is not null) - { - query = specification.Include(query); - } - - // Apply ordering if expressions are set - if (specification.OrderBy is not null) - { - query = query.OrderBy(specification.OrderBy); - } - else if (specification.OrderByDescending is not null) - { - query = query.OrderByDescending(specification.OrderByDescending); - } - - // Apply paging if enabled - if (specification.IsPagingEnabled) - { - query = query.Skip(specification.Skip) - .Take(specification.Take); - } - return query; - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Moonglade.Data.csproj b/src/Moonglade.Data/Moonglade.Data.csproj index a8bbb077c..895414053 100644 --- a/src/Moonglade.Data/Moonglade.Data.csproj +++ b/src/Moonglade.Data/Moonglade.Data.csproj @@ -10,8 +10,13 @@ - + + + - + + + + \ No newline at end of file diff --git a/src/Moonglade.Data/MoongladeRepository.cs b/src/Moonglade.Data/MoongladeRepository.cs new file mode 100644 index 000000000..015b04a10 --- /dev/null +++ b/src/Moonglade.Data/MoongladeRepository.cs @@ -0,0 +1,32 @@ +using Ardalis.Specification.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace Moonglade.Data; + +public class MoongladeRepository(BlogDbContext dbContext) : RepositoryBase(dbContext) + where T : class +{ + public IQueryable AsQueryable() => dbContext.Set(); + + public Task CountAsync(Expression> condition, CancellationToken ct = default) => + dbContext.Set().CountAsync(condition, ct); + + public Task Clear(CancellationToken ct = default) + { + dbContext.RemoveRange(dbContext.Set()); + return dbContext.SaveChangesAsync(ct); + } + + public async Task> SelectAsync(Expression> selector, CancellationToken ct = default) => + await dbContext.Set().AsNoTracking().Select(selector).ToListAsync(cancellationToken: ct); + + public async Task> SelectAsync( + ISpecification spec, Expression> selector, CancellationToken ct = default) => + await ApplySpecification(spec).AsNoTracking().Select(selector).ToListAsync(ct); + + public async Task> SelectAsync( + Expression> groupExpression, + Expression, TResult>> selector, + ISpecification spec) => + await ApplySpecification(spec).GroupBy(groupExpression).Select(selector).ToListAsync(); +} \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/PageSegment.cs b/src/Moonglade.Data/PageSegment.cs similarity index 84% rename from src/Moonglade.Core/PageFeature/PageSegment.cs rename to src/Moonglade.Data/PageSegment.cs index 806c9e615..81f7a94ce 100644 --- a/src/Moonglade.Core/PageFeature/PageSegment.cs +++ b/src/Moonglade.Data/PageSegment.cs @@ -1,4 +1,4 @@ -namespace Moonglade.Core.PageFeature; +namespace Moonglade.Data; public record PageSegment { diff --git a/src/Moonglade.Data/Seed.cs b/src/Moonglade.Data/Seed.cs index b03866f0c..d96db321b 100644 --- a/src/Moonglade.Data/Seed.cs +++ b/src/Moonglade.Data/Seed.cs @@ -11,13 +11,22 @@ public static async Task SeedAsync(BlogDbContext dbContext, ILogger logger, int try { - await dbContext.LocalAccount.AddRangeAsync(GetLocalAccounts()); + logger.LogDebug("Adding themes data..."); await dbContext.BlogTheme.AddRangeAsync(GetThemes()); + + logger.LogDebug("Adding categories data..."); await dbContext.Category.AddRangeAsync(GetCategories()); + + logger.LogDebug("Adding tags data..."); await dbContext.Tag.AddRangeAsync(GetTags()); + + logger.LogDebug("Adding friend links data..."); await dbContext.FriendLink.AddRangeAsync(GetFriendLinks()); + + logger.LogDebug("Adding pages data..."); await dbContext.CustomPage.AddRangeAsync(GetPages()); + logger.LogDebug("Adding example post..."); // Add example post var content = "Moonglade is the blog system for https://edi.wang. Powered by .NET 8 and runs on Microsoft Azure, the best cloud on the planet."; @@ -59,18 +68,6 @@ public static async Task SeedAsync(BlogDbContext dbContext, ILogger logger, int } } - private static IEnumerable GetLocalAccounts() => - new List - { - new() - { - Id = Guid.Parse("ab78493d-7569-42d2-ae78-c2b610ada1aa"), - Username = "admin", - PasswordHash = "JAvlGPq9JyTdtvBO6x2llnRI1+gxwIyPqCKAn3THIKk=", - CreateTimeUtc = DateTime.UtcNow - } - }; - private static IEnumerable GetThemes() => new List { @@ -116,7 +113,7 @@ private static IEnumerable GetCategories() => Id = Guid.Parse("b0c15707-dfc8-4b09-9aa0-5bfca744c50b"), DisplayName = "Default", Note = "Default Category", - RouteName = "default" + Slug = "default" } }; diff --git a/src/Moonglade.Data/SiteMapInfo.cs b/src/Moonglade.Data/SiteMapInfo.cs new file mode 100644 index 000000000..fe621809d --- /dev/null +++ b/src/Moonglade.Data/SiteMapInfo.cs @@ -0,0 +1,8 @@ +namespace Moonglade.Data; + +public class SiteMapInfo +{ + public string Slug { get; set; } + public DateTime CreateTimeUtc { get; set; } + public DateTime? UpdateTimeUtc { get; set; } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/CategorySpec.cs b/src/Moonglade.Data/Spec/CategorySpec.cs deleted file mode 100644 index c7abeb6c4..000000000 --- a/src/Moonglade.Data/Spec/CategorySpec.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public class CategorySpec : BaseSpecification -{ - public CategorySpec(string routeName) : base(c => c.RouteName == routeName) - { - - } - - public CategorySpec(Guid id) : base(c => c.Id == id) - { - - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/CommentReplySpec.cs b/src/Moonglade.Data/Spec/CommentReplySpec.cs deleted file mode 100644 index 1434caf04..000000000 --- a/src/Moonglade.Data/Spec/CommentReplySpec.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public class CommentReplySpec(Guid commentId) : BaseSpecification(cr => cr.CommentId == commentId); \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/CommentSpec.cs b/src/Moonglade.Data/Spec/CommentSpec.cs deleted file mode 100644 index a0764a334..000000000 --- a/src/Moonglade.Data/Spec/CommentSpec.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public sealed class CommentSpec : BaseSpecification -{ - public CommentSpec(int pageSize, int pageIndex) : base(c => true) - { - var startRow = (pageIndex - 1) * pageSize; - - AddInclude(comment => comment - .Include(c => c.Post) - .Include(c => c.Replies)); - ApplyOrderByDescending(p => p.CreateTimeUtc); - ApplyPaging(startRow, pageSize); - } - - public CommentSpec(Guid[] ids) : base(c => ids.Contains(c.Id)) - { - - } - - public CommentSpec(Guid postId) : base(c => c.PostId == postId && - c.IsApproved) - { - AddInclude(comments => comments.Include(c => c.Replies)); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/FeaturedPostSpec.cs b/src/Moonglade.Data/Spec/FeaturedPostSpec.cs deleted file mode 100644 index 4660a89c9..000000000 --- a/src/Moonglade.Data/Spec/FeaturedPostSpec.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public sealed class FeaturedPostSpec : BaseSpecification -{ - public FeaturedPostSpec() : base(p => p.IsFeatured) - { - } - - public FeaturedPostSpec(int pageSize, int pageIndex) - : base(p => - p.IsFeatured - && !p.IsDeleted - && p.IsPublished) - { - var startRow = (pageIndex - 1) * pageSize; - ApplyPaging(startRow, pageSize); - ApplyOrderByDescending(p => p.PubDateUtc); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/FriendLinkSpec.cs b/src/Moonglade.Data/Spec/FriendLinkSpec.cs deleted file mode 100644 index db186dc68..000000000 --- a/src/Moonglade.Data/Spec/FriendLinkSpec.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public class FriendLinkSpec(Guid id) : BaseSpecification(f => f.Id == id); \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/PageSpec.cs b/src/Moonglade.Data/Spec/PageSpec.cs deleted file mode 100644 index 7efe61db1..000000000 --- a/src/Moonglade.Data/Spec/PageSpec.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public sealed class PageSpec : BaseSpecification -{ - public PageSpec(int top) : base(p => p.IsPublished) - { - ApplyOrderByDescending(p => p.CreateTimeUtc); - ApplyPaging(0, top); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/PostPagingSpec.cs b/src/Moonglade.Data/Spec/PostPagingSpec.cs deleted file mode 100644 index 0ec1fd016..000000000 --- a/src/Moonglade.Data/Spec/PostPagingSpec.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public sealed class PostPagingSpec : BaseSpecification -{ - public PostPagingSpec(int pageSize, int pageIndex, Guid? categoryId = null) - : base(p => !p.IsDeleted && p.IsPublished && - (categoryId == null || p.PostCategory.Select(c => c.CategoryId).Contains(categoryId.Value))) - { - var startRow = (pageIndex - 1) * pageSize; - - ApplyOrderByDescending(p => p.PubDateUtc); - ApplyPaging(startRow, pageSize); - } - - public PostPagingSpec(PostStatus postStatus, string keyword, int pageSize, int offset) - : base(p => null == keyword || p.Title.Contains(keyword)) - { - switch (postStatus) - { - case PostStatus.Draft: - AddCriteria(p => !p.IsPublished && !p.IsDeleted); - break; - case PostStatus.Published: - AddCriteria(p => p.IsPublished && !p.IsDeleted); - break; - case PostStatus.Deleted: - AddCriteria(p => p.IsDeleted); - break; - case PostStatus.Default: - AddCriteria(p => true); - break; - default: - throw new ArgumentOutOfRangeException(nameof(postStatus), postStatus, null); - } - - ApplyPaging(offset, pageSize); - ApplyOrderByDescending(p => p.PubDateUtc); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/PostSitePageSpec.cs b/src/Moonglade.Data/Spec/PostSitePageSpec.cs deleted file mode 100644 index b66f3125e..000000000 --- a/src/Moonglade.Data/Spec/PostSitePageSpec.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public class PostSitePageSpec() : BaseSpecification(p => - p.IsPublished && !p.IsDeleted); \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/PostSpec.cs b/src/Moonglade.Data/Spec/PostSpec.cs deleted file mode 100644 index 9e751bed0..000000000 --- a/src/Moonglade.Data/Spec/PostSpec.cs +++ /dev/null @@ -1,107 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public sealed class PostSpec : BaseSpecification -{ - public PostSpec(Guid? categoryId, int? top = null) : - base(p => !p.IsDeleted && - p.IsPublished && - p.IsFeedIncluded && - (categoryId == null || p.PostCategory.Any(c => c.CategoryId == categoryId.Value))) - { - ApplyOrderByDescending(p => p.PubDateUtc); - - if (top.HasValue) - { - ApplyPaging(0, top.Value); - } - } - - public PostSpec(int year, int month = 0) : - base(p => p.PubDateUtc.Value.Year == year && - (month == 0 || p.PubDateUtc.Value.Month == month)) - { - // Fix #313: Filter out unpublished posts - AddCriteria(p => p.IsPublished && !p.IsDeleted); - - ApplyOrderByDescending(p => p.PubDateUtc); - } - - public PostSpec(string slug, DateTime pubDateUtc) : - base(p => - p.Slug == slug && - p.PubDateUtc != null - && p.PubDateUtc.Value.Year == pubDateUtc.Year - && p.PubDateUtc.Value.Month == pubDateUtc.Month - && p.PubDateUtc.Value.Day == pubDateUtc.Day) - { - - } - - public PostSpec(int hashCheckSum) - : base(p => p.HashCheckSum == hashCheckSum && p.IsPublished && !p.IsDeleted) - { - AddInclude(post => post - .Include(p => p.Comments) - .Include(pt => pt.Tags) - .Include(p => p.PostCategory).ThenInclude(pc => pc.Category)); - } - - public PostSpec(DateTime date, string slug) - : base(p => p.Slug == slug && - p.IsPublished && - p.PubDateUtc.Value.Date == date && - !p.IsDeleted) - { - AddInclude(post => post - .Include(p => p.Comments) - .Include(pt => pt.Tags) - .Include(p => p.PostCategory).ThenInclude(pc => pc.Category)); - } - - public PostSpec(Guid id, bool includeRelatedData = true) : base(p => p.Id == id) - { - if (includeRelatedData) - { - AddInclude(post => post - .Include(p => p.Tags) - .Include(p => p.PostCategory) - .ThenInclude(pc => pc.Category)); - } - } - - public PostSpec(PostStatus status) - { - switch (status) - { - case PostStatus.Draft: - AddCriteria(p => !p.IsPublished && !p.IsDeleted); - break; - case PostStatus.Published: - AddCriteria(p => p.IsPublished && !p.IsDeleted); - break; - case PostStatus.Deleted: - AddCriteria(p => p.IsDeleted); - break; - case PostStatus.Default: - AddCriteria(p => true); - break; - default: - throw new ArgumentOutOfRangeException(nameof(status), status, null); - } - } - - public PostSpec(bool isDeleted) : - base(p => p.IsDeleted == isDeleted) - { - - } - - public PostSpec() : - base(p => p.IsDeleted) - { - - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/PostTagSpec.cs b/src/Moonglade.Data/Spec/PostTagSpec.cs deleted file mode 100644 index 10e8488cf..000000000 --- a/src/Moonglade.Data/Spec/PostTagSpec.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public sealed class PostTagSpec : BaseSpecification -{ - public PostTagSpec(int tagId) : base(pt => pt.TagId == tagId) - { - } - - public PostTagSpec(int tagId, int pageSize, int pageIndex) - : base(pt => - pt.TagId == tagId - && !pt.Post.IsDeleted - && pt.Post.IsPublished) - { - var startRow = (pageIndex - 1) * pageSize; - ApplyPaging(startRow, pageSize); - ApplyOrderByDescending(p => p.Post.PubDateUtc); - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/TagSpec.cs b/src/Moonglade.Data/Spec/TagSpec.cs deleted file mode 100644 index ba4bca140..000000000 --- a/src/Moonglade.Data/Spec/TagSpec.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public sealed class TagSpec : BaseSpecification -{ - public TagSpec(int top) : base(t => true) - { - ApplyPaging(0, top); - ApplyOrderByDescending(p => p.Posts.Count); - } - - public TagSpec(string normalizedName) : base(t => t.NormalizedName == normalizedName) - { - - } -} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/BlogConfigurationSpec.cs b/src/Moonglade.Data/Specifications/BlogConfigurationSpec.cs new file mode 100644 index 000000000..fef63e7e2 --- /dev/null +++ b/src/Moonglade.Data/Specifications/BlogConfigurationSpec.cs @@ -0,0 +1,11 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public class BlogConfigurationSpec : SingleResultSpecification +{ + public BlogConfigurationSpec(string cfgKey) + { + Query.Where(p => p.CfgKey == cfgKey); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/BlogThemeForIdNameSpec.cs b/src/Moonglade.Data/Specifications/BlogThemeForIdNameSpec.cs new file mode 100644 index 000000000..608cebd03 --- /dev/null +++ b/src/Moonglade.Data/Specifications/BlogThemeForIdNameSpec.cs @@ -0,0 +1,12 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public class BlogThemeForIdNameSpec : Specification +{ + public BlogThemeForIdNameSpec() + { + Query.Select(p => new(p.Id, p.ThemeName)); + Query.AsNoTracking(); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/CategoryBySlugSpec.cs b/src/Moonglade.Data/Specifications/CategoryBySlugSpec.cs new file mode 100644 index 000000000..4a141933f --- /dev/null +++ b/src/Moonglade.Data/Specifications/CategoryBySlugSpec.cs @@ -0,0 +1,11 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public class CategoryBySlugSpec : SingleResultSpecification +{ + public CategoryBySlugSpec(string slug) + { + Query.Where(p => p.Slug == slug); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/CommentSpec.cs b/src/Moonglade.Data/Specifications/CommentSpec.cs new file mode 100644 index 000000000..ab02052d1 --- /dev/null +++ b/src/Moonglade.Data/Specifications/CommentSpec.cs @@ -0,0 +1,34 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class CommentPagingSepc : Specification +{ + public CommentPagingSepc(int pageSize, int pageIndex) + { + var startRow = (pageIndex - 1) * pageSize; + + Query.Include(c => c.Post); + Query.Include(c => c.Replies); + + Query.OrderByDescending(p => p.CreateTimeUtc); + Query.Take(pageSize).Skip(startRow); + } +} + +public sealed class CommentByIdsSepc : Specification +{ + public CommentByIdsSepc(Guid[] ids) + { + Query.Where(c => ids.Contains(c.Id)); + } +} + +public sealed class CommentWithRepliesSpec : Specification +{ + public CommentWithRepliesSpec(Guid postId) + { + Query.Where(c => c.PostId == postId && c.IsApproved); + Query.Include(p => p.Replies); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/FeaturedPostPagingSpec.cs b/src/Moonglade.Data/Specifications/FeaturedPostPagingSpec.cs new file mode 100644 index 000000000..b1be4f582 --- /dev/null +++ b/src/Moonglade.Data/Specifications/FeaturedPostPagingSpec.cs @@ -0,0 +1,18 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class FeaturedPostPagingSpec : Specification +{ + public FeaturedPostPagingSpec(int pageSize, int pageIndex) + { + Query.Where(p => + p.IsFeatured + && !p.IsDeleted + && p.IsPublished); + + var startRow = (pageIndex - 1) * pageSize; + Query.Skip(startRow).Take(pageSize); + Query.OrderByDescending(p => p.PubDateUtc); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/HotTagSpec.cs b/src/Moonglade.Data/Specifications/HotTagSpec.cs new file mode 100644 index 000000000..cb2dd4338 --- /dev/null +++ b/src/Moonglade.Data/Specifications/HotTagSpec.cs @@ -0,0 +1,14 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class HotTagSpec : Specification +{ + public HotTagSpec(int top) + { + Query.Skip(0).Take(top); + Query.OrderByDescending(p => p.Posts.Count); + + Query.Select(t => new ValueTuple(t, t.Posts.Count)); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/LoginHistorySpec.cs b/src/Moonglade.Data/Specifications/LoginHistorySpec.cs new file mode 100644 index 000000000..bba7a9d99 --- /dev/null +++ b/src/Moonglade.Data/Specifications/LoginHistorySpec.cs @@ -0,0 +1,13 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class LoginHistorySpec : Specification +{ + public LoginHistorySpec(int top) + { + Query.Skip(0).Take(top); + Query.OrderByDescending(p => p.LoginTimeUtc); + Query.AsNoTracking(); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/PageBySlugSpec.cs b/src/Moonglade.Data/Specifications/PageBySlugSpec.cs new file mode 100644 index 000000000..52b7efb66 --- /dev/null +++ b/src/Moonglade.Data/Specifications/PageBySlugSpec.cs @@ -0,0 +1,11 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public class PageBySlugSpec : SingleResultSpecification +{ + public PageBySlugSpec(string slug) + { + Query.Where(p => p.Slug == slug); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/PageSegmentSpec.cs b/src/Moonglade.Data/Specifications/PageSegmentSpec.cs new file mode 100644 index 000000000..48a0d1f97 --- /dev/null +++ b/src/Moonglade.Data/Specifications/PageSegmentSpec.cs @@ -0,0 +1,19 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class PageSegmentSpec : Specification +{ + public PageSegmentSpec() + { + Query.Select(page => new PageSegment + { + Id = page.Id, + CreateTimeUtc = page.CreateTimeUtc, + Slug = page.Slug, + Title = page.Title, + IsPublished = page.IsPublished + }); + Query.AsNoTracking(); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/PageSitemapSpec.cs b/src/Moonglade.Data/Specifications/PageSitemapSpec.cs new file mode 100644 index 000000000..0b7a787fa --- /dev/null +++ b/src/Moonglade.Data/Specifications/PageSitemapSpec.cs @@ -0,0 +1,18 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class PageSitemapSpec : Specification +{ + public PageSitemapSpec() + { + Query.Where(p => p.IsPublished); + Query.Select(p => new SiteMapInfo + { + Slug = p.Slug, + CreateTimeUtc = p.CreateTimeUtc, + UpdateTimeUtc = p.UpdateTimeUtc + }); + Query.AsNoTracking(); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/PingbackReadOnlySpec.cs b/src/Moonglade.Data/Specifications/PingbackReadOnlySpec.cs new file mode 100644 index 000000000..7fd89158c --- /dev/null +++ b/src/Moonglade.Data/Specifications/PingbackReadOnlySpec.cs @@ -0,0 +1,11 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public class PingbackReadOnlySpec : Specification +{ + public PingbackReadOnlySpec() + { + Query.AsNoTracking(); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/PingbackSpec.cs b/src/Moonglade.Data/Specifications/PingbackSpec.cs new file mode 100644 index 000000000..42d2a6e49 --- /dev/null +++ b/src/Moonglade.Data/Specifications/PingbackSpec.cs @@ -0,0 +1,14 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class PingbackSpec : Specification +{ + public PingbackSpec(Guid postId, string sourceUrl, string sourceIp) + { + Query.Where(p => + p.TargetPostId == postId && + p.SourceUrl == sourceUrl && + p.SourceIp == sourceIp); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/PostPagingSpec.cs b/src/Moonglade.Data/Specifications/PostPagingSpec.cs new file mode 100644 index 000000000..181f75608 --- /dev/null +++ b/src/Moonglade.Data/Specifications/PostPagingSpec.cs @@ -0,0 +1,46 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class PostPagingSpec : Specification +{ + public PostPagingSpec(int pageSize, int pageIndex, Guid? categoryId = null) + { + Query.Where(p => !p.IsDeleted && p.IsPublished && + (categoryId == null || p.PostCategory.Select(c => c.CategoryId).Contains(categoryId.Value))); + + var startRow = (pageIndex - 1) * pageSize; + + Query.OrderByDescending(p => p.PubDateUtc); + Query.Skip(startRow).Take(pageSize); + } +} + +public sealed class PostPagingByStatusSpec : Specification +{ + public PostPagingByStatusSpec(PostStatus postStatus, string keyword, int pageSize, int offset) + { + Query.Where(p => null == keyword || p.Title.Contains(keyword)); + + switch (postStatus) + { + case PostStatus.Draft: + Query.Where(p => !p.IsPublished && !p.IsDeleted); + break; + case PostStatus.Published: + Query.Where(p => p.IsPublished && !p.IsDeleted); + break; + case PostStatus.Deleted: + Query.Where(p => p.IsDeleted); + break; + case PostStatus.Default: + Query.Where(p => true); + break; + default: + throw new ArgumentOutOfRangeException(nameof(postStatus), postStatus, null); + } + + Query.Skip(offset).Take(pageSize); + Query.OrderByDescending(p => p.PubDateUtc); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/PostSiteMapSpec.cs b/src/Moonglade.Data/Specifications/PostSiteMapSpec.cs new file mode 100644 index 000000000..57ed11803 --- /dev/null +++ b/src/Moonglade.Data/Specifications/PostSiteMapSpec.cs @@ -0,0 +1,25 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class PostSiteMapSpec : Specification +{ + public PostSiteMapSpec() + { + Query.Where(p => p.IsPublished && !p.IsDeleted); + Query.Select(p => new PostSiteMapInfo + { + Slug = p.Slug, + CreateTimeUtc = p.PubDateUtc.GetValueOrDefault(), + UpdateTimeUtc = p.LastModifiedUtc + }); + Query.AsNoTracking(); + } +} + +public class PostSiteMapInfo +{ + public string Slug { get; set; } + public DateTime CreateTimeUtc { get; set; } + public DateTime? UpdateTimeUtc { get; set; } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/PostSpec.cs b/src/Moonglade.Data/Specifications/PostSpec.cs new file mode 100644 index 000000000..dee80f202 --- /dev/null +++ b/src/Moonglade.Data/Specifications/PostSpec.cs @@ -0,0 +1,148 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class PostSpec : Specification +{ + public PostSpec(Guid id, bool includeRelatedData = true) + { + Query.Where(p => p.Id == id); + + if (includeRelatedData) + { + Query.Include(p => p.Tags) + .Include(p => p.PostCategory) + .ThenInclude(pc => pc.Category); + } + } +} + +public sealed class PostByIdForTitleDateSpec : SingleResultSpecification +{ + public PostByIdForTitleDateSpec(Guid id) + { + Query.Where(p => p.Id == id); + Query.Select(p => new ValueTuple(p.Title, p.PubDateUtc)); + } +} + +public sealed class PostByStatusSpec : Specification +{ + public PostByStatusSpec(PostStatus status) + { + switch (status) + { + case PostStatus.Draft: + Query.Where(p => !p.IsPublished && !p.IsDeleted); + break; + case PostStatus.Published: + Query.Where(p => p.IsPublished && !p.IsDeleted); + break; + case PostStatus.Deleted: + Query.Where(p => p.IsDeleted); + break; + case PostStatus.Default: + Query.Where(p => true); + break; + default: + throw new ArgumentOutOfRangeException(nameof(status), status, null); + } + + Query.AsNoTracking(); + } +} + +public sealed class FeaturedPostSpec : Specification +{ + public FeaturedPostSpec() + { + Query.Where(p => p.IsFeatured && p.IsPublished && !p.IsDeleted); + } +} + +public sealed class PostByCatSpec : Specification +{ + public PostByCatSpec(Guid? categoryId, int? top = null) + { + Query.Where(p => + !p.IsDeleted && + p.IsPublished && + p.IsFeedIncluded && + (categoryId == null || p.PostCategory.Any(c => c.CategoryId == categoryId.Value))); + + Query.OrderByDescending(p => p.PubDateUtc); + + if (top.HasValue) + { + Query.Skip(0).Take(top.Value); + } + } +} + +public sealed class PostByYearMonthSpec : Specification +{ + public PostByYearMonthSpec(int year, int month = 0) + { + Query.Where(p => p.PubDateUtc.Value.Year == year && + (month == 0 || p.PubDateUtc.Value.Month == month)); + + // Fix #313: Filter out unpublished posts + Query.Where(p => p.IsPublished && !p.IsDeleted); + + Query.OrderByDescending(p => p.PubDateUtc); + Query.AsNoTracking(); + } +} + +public sealed class PostByDeletionFlagSpec : Specification +{ + public PostByDeletionFlagSpec(bool isDeleted) => Query.Where(p => p.IsDeleted == isDeleted); +} + +public sealed class PostByChecksumSpec : SingleResultSpecification +{ + public PostByChecksumSpec(int hashCheckSum) + { + Query.Where(p => p.HashCheckSum == hashCheckSum && p.IsPublished && !p.IsDeleted); + + Query.Include(p => p.Comments) + .Include(pt => pt.Tags) + .Include(p => p.PostCategory) + .ThenInclude(pc => pc.Category) + .AsSplitQuery(); + } +} + +public sealed class PostByDateAndSlugSpec : Specification +{ + public PostByDateAndSlugSpec(DateTime date, string slug, bool includeRelationData) + { + Query.Where(p => + p.Slug == slug && + p.IsPublished && + p.PubDateUtc.Value.Date == date && + !p.IsDeleted); + + if (includeRelationData) + { + Query.Include(p => p.Comments) + .Include(pt => pt.Tags) + .Include(p => p.PostCategory) + .ThenInclude(pc => pc.Category); + } + } +} + +public sealed class PostByDateAndSlugForIdTitleSpec : SingleResultSpecification +{ + public PostByDateAndSlugForIdTitleSpec(DateTime date, string slug) + { + Query.Where(p => + p.Slug == slug && + p.IsPublished && + p.PubDateUtc.Value.Date == date && + !p.IsDeleted); + + Query.Select(p => new ValueTuple(p.Id, p.Title)); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Spec/PostStatus.cs b/src/Moonglade.Data/Specifications/PostStatus.cs similarity index 68% rename from src/Moonglade.Data/Spec/PostStatus.cs rename to src/Moonglade.Data/Specifications/PostStatus.cs index 2a95fb001..c41c31139 100644 --- a/src/Moonglade.Data/Spec/PostStatus.cs +++ b/src/Moonglade.Data/Specifications/PostStatus.cs @@ -1,4 +1,4 @@ -namespace Moonglade.Data.Spec; +namespace Moonglade.Data.Specifications; public enum PostStatus { diff --git a/src/Moonglade.Data/Specifications/PostTagSpec.cs b/src/Moonglade.Data/Specifications/PostTagSpec.cs new file mode 100644 index 000000000..9e2eca9c4 --- /dev/null +++ b/src/Moonglade.Data/Specifications/PostTagSpec.cs @@ -0,0 +1,26 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class PostTagSpec : Specification +{ + public PostTagSpec(int tagId, int pageSize, int pageIndex) + { + Query.Where(pt => + pt.TagId == tagId + && !pt.Post.IsDeleted + && pt.Post.IsPublished); + + var startRow = (pageIndex - 1) * pageSize; + Query.Skip(startRow).Take(pageSize); + Query.OrderByDescending(p => p.Post.PubDateUtc); + } +} + +public class PostTagByTagIdSpec : SingleResultSpecification +{ + public PostTagByTagIdSpec(int tagId) + { + Query.Where(pt => pt.TagId == tagId); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/TagByDisplayNameSpec.cs b/src/Moonglade.Data/Specifications/TagByDisplayNameSpec.cs new file mode 100644 index 000000000..b5c4cec15 --- /dev/null +++ b/src/Moonglade.Data/Specifications/TagByDisplayNameSpec.cs @@ -0,0 +1,11 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class TagByDisplayNameSpec : SingleResultSpecification +{ + public TagByDisplayNameSpec(string displayName) + { + Query.Where(t => t.DisplayName == displayName); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/TagByNormalizedNameSpec.cs b/src/Moonglade.Data/Specifications/TagByNormalizedNameSpec.cs new file mode 100644 index 000000000..743de96a9 --- /dev/null +++ b/src/Moonglade.Data/Specifications/TagByNormalizedNameSpec.cs @@ -0,0 +1,11 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class TagByNormalizedNameSpec : SingleResultSpecification +{ + public TagByNormalizedNameSpec(string normalizedName) + { + Query.Where(t => t.NormalizedName == normalizedName); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/TagCloudSpec.cs b/src/Moonglade.Data/Specifications/TagCloudSpec.cs new file mode 100644 index 000000000..9b6d89bc6 --- /dev/null +++ b/src/Moonglade.Data/Specifications/TagCloudSpec.cs @@ -0,0 +1,11 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class TagCloudSpec : Specification +{ + public TagCloudSpec() + { + Query.Select(t => new ValueTuple(t, t.Posts.Count)); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/TagDisplayNameNameSpec.cs b/src/Moonglade.Data/Specifications/TagDisplayNameNameSpec.cs new file mode 100644 index 000000000..543ae6262 --- /dev/null +++ b/src/Moonglade.Data/Specifications/TagDisplayNameNameSpec.cs @@ -0,0 +1,11 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class TagDisplayNameNameSpec : Specification +{ + public TagDisplayNameNameSpec() + { + Query.Select(t => t.DisplayName); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/Specifications/ThemeByNameSpec.cs b/src/Moonglade.Data/Specifications/ThemeByNameSpec.cs new file mode 100644 index 000000000..e5fa1723a --- /dev/null +++ b/src/Moonglade.Data/Specifications/ThemeByNameSpec.cs @@ -0,0 +1,11 @@ +using Moonglade.Data.Entities; + +namespace Moonglade.Data.Specifications; + +public sealed class ThemeByNameSpec : Specification +{ + public ThemeByNameSpec(string name) + { + Query.Where(p => p.ThemeName == name); + } +} \ No newline at end of file diff --git a/src/Moonglade.Data/ThemeSegment.cs b/src/Moonglade.Data/ThemeSegment.cs new file mode 100644 index 000000000..532e21fc3 --- /dev/null +++ b/src/Moonglade.Data/ThemeSegment.cs @@ -0,0 +1,3 @@ +namespace Moonglade.Data; + +public record ThemeSegment(int Id, string Name); \ No newline at end of file diff --git a/src/Moonglade.Email.Client/ServiceCollectionExtension.cs b/src/Moonglade.Email.Client/ServiceCollectionExtension.cs index f247499d3..eb968cae6 100644 --- a/src/Moonglade.Email.Client/ServiceCollectionExtension.cs +++ b/src/Moonglade.Email.Client/ServiceCollectionExtension.cs @@ -4,7 +4,7 @@ namespace Moonglade.Email.Client; public static class ServiceCollectionExtension { - public static IServiceCollection AddEmailSending(this IServiceCollection services) + public static IServiceCollection AddEmailClient(this IServiceCollection services) { services.AddHttpClient(); return services; diff --git a/src/Moonglade.FriendLink/AddLinkCommand.cs b/src/Moonglade.FriendLink/AddLinkCommand.cs index 310ff6555..0401291d5 100644 --- a/src/Moonglade.FriendLink/AddLinkCommand.cs +++ b/src/Moonglade.FriendLink/AddLinkCommand.cs @@ -1,12 +1,13 @@ using MediatR; +using Microsoft.Extensions.Logging; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; using Moonglade.Utils; using System.ComponentModel.DataAnnotations; namespace Moonglade.FriendLink; -public class AddLinkCommand : IRequest, IValidatableObject +public class EditLinkRequest : IValidatableObject { [Required] [Display(Name = "Title")] @@ -31,18 +32,24 @@ public IEnumerable Validate(ValidationContext validationContex } } -public class AddLinkCommandHandler(IRepository repo) : IRequestHandler +public record AddLinkCommand(EditLinkRequest Payload) : IRequest; + +public class AddLinkCommandHandler( + MoongladeRepository repo, + ILogger logger) : IRequestHandler { public async Task Handle(AddLinkCommand request, CancellationToken ct) { var link = new FriendLinkEntity { Id = Guid.NewGuid(), - LinkUrl = Helper.SterilizeLink(request.LinkUrl), - Title = request.Title, - Rank = request.Rank + LinkUrl = Helper.SterilizeLink(request.Payload.LinkUrl), + Title = request.Payload.Title, + Rank = request.Payload.Rank }; await repo.AddAsync(link, ct); + + logger.LogInformation("Added a new friend link: {Title}", link.Title); } } \ No newline at end of file diff --git a/src/Moonglade.FriendLink/DeleteLinkCommand.cs b/src/Moonglade.FriendLink/DeleteLinkCommand.cs index a54dadfeb..21c19807f 100644 --- a/src/Moonglade.FriendLink/DeleteLinkCommand.cs +++ b/src/Moonglade.FriendLink/DeleteLinkCommand.cs @@ -1,12 +1,24 @@ using MediatR; +using Microsoft.Extensions.Logging; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; namespace Moonglade.FriendLink; public record DeleteLinkCommand(Guid Id) : IRequest; -public class DeleteLinkCommandHandler(IRepository repo) : IRequestHandler +public class DeleteLinkCommandHandler( + MoongladeRepository repo, + ILogger logger) : IRequestHandler { - public Task Handle(DeleteLinkCommand request, CancellationToken ct) => repo.DeleteAsync(request.Id, ct); + public async Task Handle(DeleteLinkCommand request, CancellationToken ct) + { + var link = await repo.GetByIdAsync(request.Id, ct); + if (null != link) + { + await repo.DeleteAsync(link, ct); + } + + logger.LogInformation("Deleted a friend link: {Title}", link?.Title); + } } \ No newline at end of file diff --git a/src/Moonglade.FriendLink/GetAllLinksQuery.cs b/src/Moonglade.FriendLink/GetAllLinksQuery.cs index 393469629..1f0e5ce0a 100644 --- a/src/Moonglade.FriendLink/GetAllLinksQuery.cs +++ b/src/Moonglade.FriendLink/GetAllLinksQuery.cs @@ -1,14 +1,14 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; namespace Moonglade.FriendLink; -public record GetAllLinksQuery : IRequest>; +public record GetAllLinksQuery : IRequest>; -public class GetAllLinksQueryHandler(IRepository repo) : IRequestHandler> +public class GetAllLinksQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(GetAllLinksQuery request, CancellationToken ct) + public Task> Handle(GetAllLinksQuery request, CancellationToken ct) { return repo.ListAsync(ct); } diff --git a/src/Moonglade.FriendLink/GetLinkQuery.cs b/src/Moonglade.FriendLink/GetLinkQuery.cs index 04860fe58..baed33a76 100644 --- a/src/Moonglade.FriendLink/GetLinkQuery.cs +++ b/src/Moonglade.FriendLink/GetLinkQuery.cs @@ -1,15 +1,12 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; namespace Moonglade.FriendLink; public record GetLinkQuery(Guid Id) : IRequest; -public class GetLinkQueryHandler(IRepository repo) : IRequestHandler +public class GetLinkQueryHandler(MoongladeRepository repo) : IRequestHandler { - public async Task Handle(GetLinkQuery request, CancellationToken ct) - { - return await repo.GetAsync(request.Id, ct); - } + public async Task Handle(GetLinkQuery request, CancellationToken ct) => await repo.GetByIdAsync(request.Id, ct); } \ No newline at end of file diff --git a/src/Moonglade.FriendLink/UpdateLinkCommand.cs b/src/Moonglade.FriendLink/UpdateLinkCommand.cs index 743cc890e..f11dcd4e1 100644 --- a/src/Moonglade.FriendLink/UpdateLinkCommand.cs +++ b/src/Moonglade.FriendLink/UpdateLinkCommand.cs @@ -1,32 +1,34 @@ using MediatR; +using Microsoft.Extensions.Logging; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; using Moonglade.Utils; namespace Moonglade.FriendLink; -public class UpdateLinkCommand : AddLinkCommand -{ - public Guid Id { get; set; } -} +public record UpdateLinkCommand(Guid Id, EditLinkRequest Payload) : IRequest; -public class UpdateLinkCommandHandler(IRepository repo) : IRequestHandler +public class UpdateLinkCommandHandler( + MoongladeRepository repo, + ILogger logger) : IRequestHandler { public async Task Handle(UpdateLinkCommand request, CancellationToken ct) { - if (!Uri.IsWellFormedUriString(request.LinkUrl, UriKind.Absolute)) + if (!Uri.IsWellFormedUriString(request.Payload.LinkUrl, UriKind.Absolute)) { - throw new InvalidOperationException($"{nameof(request.LinkUrl)} is not a valid url."); + throw new InvalidOperationException($"{nameof(request.Payload.LinkUrl)} is not a valid url."); } - var link = await repo.GetAsync(request.Id, ct); + var link = await repo.GetByIdAsync(request.Id, ct); if (link is not null) { - link.Title = request.Title; - link.LinkUrl = Helper.SterilizeLink(request.LinkUrl); - link.Rank = request.Rank; + link.Title = request.Payload.Title; + link.LinkUrl = Helper.SterilizeLink(request.Payload.LinkUrl); + link.Rank = request.Payload.Rank; await repo.UpdateAsync(link, ct); } + + logger.LogInformation("Updated link: {LinkId}", request.Id); } } \ No newline at end of file diff --git a/src/Moonglade.MetaWeblog/DateTime8601.cs b/src/Moonglade.MetaWeblog/DateTime8601.cs deleted file mode 100644 index d8775209f..000000000 --- a/src/Moonglade.MetaWeblog/DateTime8601.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Globalization; -using System.Text.RegularExpressions; - -namespace Moonglade.MetaWeblog; -// Borrowed from: https://github.com/snielsson/XmlRpcLight/blob/master/XmlRpcLight/DataTypes/DateTime8601.cs - -internal static class DateTime8601 -{ - private static readonly Regex DateTime8601Regex = new( - @"(((?\d{4})-(?\d{2})-(?\d{2}))|((?\d{4})(?\d{2})(?\d{2})))" - + @"T" - + @"(((?\d{2}):(?\d{2}):(?\d{2}))|((?\d{2})(?\d{2})(?\d{2})))" - + @"(?$|Z|([+-]\d{2}:?(\d{2})?))"); - - public static bool TryParseDateTime8601(string date, out DateTime result) - { - result = DateTime.MinValue; - var m = DateTime8601Regex.Match(date); - var normalized = m.Groups["year"].Value + m.Groups["month"].Value + m.Groups["day"].Value - + "T" - + m.Groups["hour"].Value + m.Groups["minute"].Value + m.Groups["second"].Value - + m.Groups["tz"].Value; - var formats = new[] { - "yyyyMMdd'T'HHmmss", - "yyyyMMdd'T'HHmmss'Z'", - "yyyyMMdd'T'HHmmsszzz", - "yyyyMMdd'T'HHmmsszz" - }; - - try - { - result = DateTime.ParseExact(normalized, formats, CultureInfo.InvariantCulture, - DateTimeStyles.AdjustToUniversal); - return true; - } - catch (Exception) - { - return false; - } - } -} \ No newline at end of file diff --git a/src/Moonglade.MetaWeblog/IMetaWeblogProvider.cs b/src/Moonglade.MetaWeblog/IMetaWeblogProvider.cs deleted file mode 100644 index 9ea75c104..000000000 --- a/src/Moonglade.MetaWeblog/IMetaWeblogProvider.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Moonglade.MetaWeblog; - -public interface IMetaWeblogProvider -{ - Task GetUserInfoAsync(string key, string username, string password); - - Task GetUsersBlogsAsync(string key, string username, string password); - - Task GetPostAsync(string postid, string username, string password); - - Task GetRecentPostsAsync(string blogid, string username, string password, int numberOfPosts); - - Task AddPostAsync(string blogid, string username, string password, Post post, bool publish); - - Task DeletePostAsync(string key, string postid, string username, string password, bool publish); - - Task EditPostAsync(string postid, string username, string password, Post post, bool publish); - - Task GetCategoriesAsync(string blogid, string username, string password); - - Task AddCategoryAsync(string key, string username, string password, NewCategory category); - - Task GetTagsAsync(string blogid, string username, string password); - - Task NewMediaObjectAsync(string blogid, string username, string password, MediaObject mediaObject); - - Task GetPageAsync(string blogid, string pageid, string username, string password); - Task GetPagesAsync(string blogid, string username, string password, int numPages); - Task GetAuthorsAsync(string blogid, string username, string password); - - Task AddPageAsync(string blogid, string username, string password, Page page, bool publish); - Task EditPageAsync(string blogid, string pageid, string username, string password, Page page, bool publish); - Task DeletePageAsync(string blogid, string username, string password, string pageid); -} \ No newline at end of file diff --git a/src/Moonglade.MetaWeblog/MetaWeblogException.cs b/src/Moonglade.MetaWeblog/MetaWeblogException.cs deleted file mode 100644 index ab0e1df6d..000000000 --- a/src/Moonglade.MetaWeblog/MetaWeblogException.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Moonglade.MetaWeblog; - -public class MetaWeblogException(string message, int code = 1) : Exception(message) -{ - public int Code { get; private set; } = code; -} \ No newline at end of file diff --git a/src/Moonglade.MetaWeblog/MetaWeblogExtensions.cs b/src/Moonglade.MetaWeblog/MetaWeblogExtensions.cs deleted file mode 100644 index 509f621a1..000000000 --- a/src/Moonglade.MetaWeblog/MetaWeblogExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace Moonglade.MetaWeblog; - -public static class MetaWeblogExtensions -{ - public static IApplicationBuilder UseMetaWeblog(this IApplicationBuilder builder, string path) - { - return builder.UseMiddleware(path); - } - - public static IServiceCollection AddMetaWeblog(this IServiceCollection coll) where TImplementation : class, IMetaWeblogProvider - { - return coll.AddScoped() - .AddScoped(); - } - -} \ No newline at end of file diff --git a/src/Moonglade.MetaWeblog/MetaWeblogMiddleware.cs b/src/Moonglade.MetaWeblog/MetaWeblogMiddleware.cs deleted file mode 100644 index 9e10479b7..000000000 --- a/src/Moonglade.MetaWeblog/MetaWeblogMiddleware.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Moonglade.MetaWeblog; - -public class MetaWeblogMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, string urlEndpoint) -{ - private readonly ILogger _logger = loggerFactory.CreateLogger(); - - public async Task Invoke(HttpContext context, MetaWeblogService service) - { - if (context.Request.Method == "POST" && - context.Request.Path.StartsWithSegments(urlEndpoint) && - context.Request.ContentType.ToLower().Contains("text/xml")) - { - try - { - context.Response.ContentType = "text/xml"; - var rdr = new StreamReader(context.Request.Body); - var xml = await rdr.ReadToEndAsync(); - _logger.LogInformation($"Request XMLRPC: {xml}"); - var result = await service.InvokeAsync(xml); - _logger.LogInformation($"Result XMLRPC: {result}"); - await context.Response.WriteAsync(result, Encoding.UTF8); - } - catch (Exception ex) - { - _logger.LogError($"Failed to read the content: {ex}"); - } - return; - } - - // Continue On - await next.Invoke(context); - } -} \ No newline at end of file diff --git a/src/Moonglade.MetaWeblog/MetaWeblogService.cs b/src/Moonglade.MetaWeblog/MetaWeblogService.cs deleted file mode 100644 index 919f53094..000000000 --- a/src/Moonglade.MetaWeblog/MetaWeblogService.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace Moonglade.MetaWeblog; - -public class MetaWeblogService(IMetaWeblogProvider provider, ILogger logger) - : XmlRpcService(logger) -{ - [XmlRpcMethod("blogger.getUsersBlogs")] - public async Task GetUsersBlogsAsync(string key, string username, string password) - { - logger.LogInformation($"MetaWeblog:GetUserBlogs is called"); - return await provider.GetUsersBlogsAsync(key, username, password); - } - - [XmlRpcMethod("blogger.getUserInfo")] - public async Task GetUserInfoAsync(string key, string username, string password) - { - logger.LogInformation($"MetaWeblog:GetUserInfo is called"); - return await provider.GetUserInfoAsync(key, username, password); - } - - [XmlRpcMethod("wp.newCategory")] - public async Task AddCategoryAsync(string key, string username, string password, NewCategory category) - { - logger.LogInformation($"MetaWeblog:AddCategory is called"); - return await provider.AddCategoryAsync(key, username, password, category); - } - - [XmlRpcMethod("metaWeblog.getPost")] - public async Task GetPostAsync(string postid, string username, string password) - { - logger.LogInformation($"MetaWeblog:GetPost is called"); - return await provider.GetPostAsync(postid, username, password); - } - - [XmlRpcMethod("metaWeblog.getRecentPosts")] - public async Task GetRecentPostsAsync(string blogid, string username, string password, int numberOfPosts) - { - logger.LogInformation($"MetaWeblog:GetRecentPosts is called"); - return await provider.GetRecentPostsAsync(blogid, username, password, numberOfPosts); - } - - [XmlRpcMethod("metaWeblog.newPost")] - public async Task AddPostAsync(string blogid, string username, string password, Post post, bool publish) - { - logger.LogInformation($"MetaWeblog:AddPost is called"); - return await provider.AddPostAsync(blogid, username, password, post, publish); - } - - [XmlRpcMethod("metaWeblog.editPost")] - public async Task EditPostAsync(string postid, string username, string password, Post post, bool publish) - { - logger.LogInformation($"MetaWeblog:EditPost is called"); - return await provider.EditPostAsync(postid, username, password, post, publish); - } - - [XmlRpcMethod("blogger.deletePost")] - public async Task DeletePostAsync(string key, string postid, string username, string password, bool publish) - { - logger.LogInformation($"MetaWeblog:DeletePost is called"); - return await provider.DeletePostAsync(key, postid, username, password, publish); - } - - [XmlRpcMethod("metaWeblog.getCategories")] - public async Task GetCategoriesAsync(string blogid, string username, string password) - { - logger.LogInformation($"MetaWeblog:GetCategories is called"); - return await provider.GetCategoriesAsync(blogid, username, password); - } - - [XmlRpcMethod("wp.getTags")] - public async Task GetTagsAsync(string blogid, string username, string password) - { - logger.LogInformation($"MetaWeblog:GetTagsAsync is called"); - return await provider.GetTagsAsync(blogid, username, password); - } - - [XmlRpcMethod("metaWeblog.newMediaObject")] - public async Task NewMediaObjectAsync(string blogid, string username, string password, MediaObject mediaObject) - { - logger.LogInformation($"MetaWeblog:NewMediaObject is called"); - return await provider.NewMediaObjectAsync(blogid, username, password, mediaObject); - } - - [XmlRpcMethod("wp.getPage")] - public async Task GetPageAsync(string blogid, string pageid, string username, string password) - { - logger.LogInformation($"wp.getPage is called"); - return await provider.GetPageAsync(blogid, pageid, username, password); - } - - [XmlRpcMethod("wp.getPages")] - public async Task GetPagesAsync(string blogid, string username, string password, int numPages) - { - logger.LogInformation($"wp.getPages is called"); - return await provider.GetPagesAsync(blogid, username, password, numPages); - } - - [XmlRpcMethod("wp.getAuthors")] - public async Task GetAuthorsAsync(string blogid, string username, string password) - { - logger.LogInformation($"wp.getAuthors is called"); - return await provider.GetAuthorsAsync(blogid, username, password); - } - - [XmlRpcMethod("wp.newPage")] - public async Task AddPageAsync(string blogid, string username, string password, Page page, bool publish) - { - logger.LogInformation($"wp.newPage is called"); - return await provider.AddPageAsync(blogid, username, password, page, publish); - } - - [XmlRpcMethod("wp.editPage")] - public async Task EditPageAsync(string blogid, string pageid, string username, string password, Page page, bool publish) - { - logger.LogInformation($"wp.editPage is called"); - return await provider.EditPageAsync(blogid, pageid, username, password, page, publish); - } - - [XmlRpcMethod("wp.deletePage")] - public async Task DeletePageAsync(string blogid, string username, string password, string pageid) - { - logger.LogInformation($"wp.deletePage is called"); - return await provider.DeletePageAsync(blogid, username, password, pageid); - } -} \ No newline at end of file diff --git a/src/Moonglade.MetaWeblog/Moonglade.MetaWeblog.csproj b/src/Moonglade.MetaWeblog/Moonglade.MetaWeblog.csproj deleted file mode 100644 index beb10f8f7..000000000 --- a/src/Moonglade.MetaWeblog/Moonglade.MetaWeblog.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0 - enable - Continue to maintain https://github.com/shawnwildermuth/MetaWeblog - - - - - - - - diff --git a/src/Moonglade.MetaWeblog/Structs.cs b/src/Moonglade.MetaWeblog/Structs.cs deleted file mode 100644 index 7ec9bf336..000000000 --- a/src/Moonglade.MetaWeblog/Structs.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ReSharper disable InconsistentNaming -namespace Moonglade.MetaWeblog; - -public class BlogInfo -{ - public string blogid; - public string url; - public string blogName; -} - -public class CategoryInfo -{ - public string description; - public string htmlUrl; - public string rssUrl; - public string title; - public string categoryid; -} - -public class NewCategory -{ - public string name; - public int parent_id; - public string slug; - public string description; -} - -public class Tag -{ - public string name; -} - -public class Enclosure -{ - public int length; - public string type; - public string url; -} - -public class Post -{ - public DateTime dateCreated; - public string description; - public string title; - public string[] categories; - public string permalink; - public object postid; - public string userid; - public string wp_slug; - public string mt_excerpt; - public string mt_keywords; - public string link; - public string wp_post_thumbnail; - public int mt_allow_comments; - public string mt_basename; -} - -public class Source -{ - public string name; - public string url; -} - -public class UserInfo -{ - public string userid; - public string firstname; - public string lastname; - public string nickname; - public string email; - public string url; -} - -public class MediaObject -{ - public string name; - public string type; - public string bits; -} - -public class MediaObjectInfo -{ - public string url; -} - -public class Page -{ - public DateTime dateCreated; - public string description; - public string title; - public string[] categories; - public string wp_author_id; - public string page_parent_id; - public string page_id; -} - -public class Author -{ - public string user_id; - public string user_login; - public string display_name; - public string meta_value; -} \ No newline at end of file diff --git a/src/Moonglade.MetaWeblog/XmlRpcMethodAttribute.cs b/src/Moonglade.MetaWeblog/XmlRpcMethodAttribute.cs deleted file mode 100644 index 869d5f133..000000000 --- a/src/Moonglade.MetaWeblog/XmlRpcMethodAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Moonglade.MetaWeblog; - -public class XmlRpcMethodAttribute(string methodName) : Attribute -{ - public string MethodName { get; set; } = methodName; -} \ No newline at end of file diff --git a/src/Moonglade.MetaWeblog/XmlRpcService.cs b/src/Moonglade.MetaWeblog/XmlRpcService.cs deleted file mode 100644 index 64c44a369..000000000 --- a/src/Moonglade.MetaWeblog/XmlRpcService.cs +++ /dev/null @@ -1,348 +0,0 @@ -using System.Collections; -using System.Diagnostics; -using System.Globalization; -using System.Reflection; -using System.Xml.Linq; -using Microsoft.Extensions.Logging; - -namespace Moonglade.MetaWeblog; - -public class XmlRpcService(ILogger logger) -{ - private string _method; - - public async Task InvokeAsync(string xml) - { - try - { - var doc = XDocument.Parse(xml); - var methodNameElement = doc - .Descendants("methodName") - .FirstOrDefault(); - if (methodNameElement != null) - { - _method = methodNameElement.Value; - - logger.LogDebug($"Invoking {_method} on XMLRPC Service"); - - var theType = GetType(); - - foreach (var typeMethod in theType.GetMethods()) - { - var attr = typeMethod.GetCustomAttribute(); - if (attr != null && _method.Equals(attr.MethodName, StringComparison.CurrentCultureIgnoreCase)) - { - var parameters = GetParameters(doc); - var resultTask = (Task)typeMethod.Invoke(this, parameters); - if (resultTask != null) - { - await resultTask; - - // get result via reflection - var resultProperty = resultTask.GetType().GetProperty("Result"); - if (resultProperty != null) - { - var result = resultProperty.GetValue(resultTask); - - return SerializeResponse(result); - } - } - } - } - } - } - catch (MetaWeblogException ex) - { - return SerializeResponse(ex); - } - catch (Exception ex) - { - logger.LogError($"Exception thrown during serialization: Exception: {ex}"); - return SerializeResponse(new MetaWeblogException($"Exception during XmlRpcService call: {ex.Message}")); - } - - return SerializeResponse(new MetaWeblogException("Failed to handle XmlRpcService call")); - } - - private string SerializeResponse(object result) - { - var doc = new XDocument(); - var response = new XElement("methodResponse"); - doc.Add(response); - - if (result is MetaWeblogException exception) - { - response.Add(SerializeFaultResponse(exception)); - } - else - { - var theParams = new XElement("params"); - response.Add(theParams); - - SerializeResponseParameters(theParams, result); - } - - return doc.ToString(SaveOptions.None); - } - - private XElement SerializeValue(object result) - { - var theType = result.GetType(); - var newElement = new XElement("value"); - - if (theType == typeof(int)) - { - newElement.Add(new XElement("i4", result.ToString())); - } - else if (theType == typeof(long)) - { - newElement.Add(new XElement("long", result.ToString())); - } - else if (theType == typeof(double)) - { - newElement.Add(new XElement("double", result.ToString())); - } - else if (theType == typeof(bool)) - { - newElement.Add(new XElement("boolean", ((bool)result) ? 1 : 0)); - } - else if (theType == typeof(string)) - { - newElement.Add(new XElement("string", result.ToString())); - } - else if (theType == typeof(DateTime)) - { - var date = (DateTime)result; - newElement.Add(new XElement("dateTime.iso8601", date.ToString("yyyyMMdd'T'HH':'mm':'ss", - DateTimeFormatInfo.InvariantInfo))); - } - else if (result is IEnumerable enumerable) - { - var data = new XElement("data"); - foreach (var item in enumerable) - { - data.Add(SerializeValue(item)); - } - newElement.Add(new XElement("array", data)); - } - else - { - var theStruct = new XElement("struct"); - // Reference Type - foreach (var field in theType.GetFields(BindingFlags.Public | BindingFlags.Instance)) - { - var member = new XElement("member"); - member.Add(new XElement("name", field.Name)); - var value = field.GetValue(result); - if (value != null) - { - member.Add(SerializeValue(value)); - theStruct.Add(member); - } - } - newElement.Add(theStruct); - } - - return newElement; - } - - private void SerializeResponseParameters(XElement theParams, object result) - { - theParams.Add(new XElement("param", SerializeValue(result))); - } - - private XElement CreateStringValue(string typeName, string value) - { - return new("value", new XElement(typeName, value)); - } - - private XElement SerializeFaultResponse(MetaWeblogException result) - { - return new("fault", - new XElement("value", - new XElement("struct", - new XElement("member", - new XElement("name", "faultCode"), - CreateStringValue("int", result.Code.ToString())), - new XElement("member", - new XElement("name", "faultString"), - CreateStringValue("string", result.Message))) - )); - } - - private object[] GetParameters(XDocument doc) - { - var parameters = new List(); - var paramsEle = doc.Descendants("params"); - - foreach (var p in paramsEle.Descendants("param")) - { - parameters.AddRange(ParseValue(p.Element("value"))); - } - - return parameters.ToArray(); - } - - private List ParseValue(XElement value) - { - var type = value.Descendants().FirstOrDefault(); - if (type != null) - { - var typename = type.Name.LocalName; - switch (typename) - { - case "array": - return ParseArray(type); - case "struct": - return ParseStruct(type); - case "i4": - case "int": - return ParseInt(type); - case "i8": - return ParseLong(type); - case "string": - return ParseString(type); - case "boolean": - return ParseBoolean(type); - case "double": - return ParseDouble(type); - case "dateTime.iso8601": - return ParseDateTime(type); - case "base64": - return ParseBase64(type); - } - } - - throw new MetaWeblogException("Failed to parse parameters"); - - } - - private List ParseBase64(XElement type) - { - return [type.Value]; - } - - private List ParseLong(XElement type) - { - return [long.Parse(type.Value)]; - } - - private List ParseDateTime(XElement type) - { - if (DateTime8601.TryParseDateTime8601(type.Value, out var parsed)) - { - return [parsed]; - } - - throw new MetaWeblogException("Failed to parse date"); - } - - private static List ParseBoolean(XElement type) - { - return [type.Value == "1"]; - } - - private static List ParseString(XElement type) - { - return [type.Value]; - } - - private List ParseDouble(XElement type) - { - return [double.Parse(type.Value)]; - } - - private List ParseInt(XElement type) - { - return [int.Parse(type.Value)]; - } - - private List ParseStruct(XElement type) - { - var dict = new Dictionary(); - var members = type.Descendants("member"); - foreach (var member in members) - { - var name = member.Element("name").Value; - var value = ParseValue(member.Element("value")); - dict[name] = value; - } - - return _method switch - { - "metaWeblog.newMediaObject" => ConvertToType(dict), - "metaWeblog.newPost" or "metaWeblog.editPost" => ConvertToType(dict), - "wp.newCategory" => ConvertToType(dict), - "wp.newPage" or "wp.editPage" => ConvertToType(dict), - _ => throw new InvalidOperationException("Unknown type of struct discovered."), - }; - } - - private List ConvertToType(Dictionary dict) where T : new() - { - var info = typeof(T).GetTypeInfo(); - - // Convert it - var result = new T(); - foreach (var key in dict.Keys) - { - var field = info.GetDeclaredField(key); - if (field != null) - { - var container = (List)dict[key]; - var value = container.Count == 1 ? container.First() : container.ToArray(); - if (field.FieldType != value.GetType()) - { - if (field.FieldType.IsArray && value.GetType().IsArray) - { - var valueArray = (Array)value; - var newValue = Array.CreateInstance(field.FieldType.GetElementType(), valueArray.Length); - Array.Copy(valueArray, newValue, valueArray.Length); - value = newValue; - } - else if (value.GetType().IsAssignableFrom(field.FieldType)) - { - value = Convert.ChangeType(value, field.FieldType); - } - else - { - logger.LogWarning($"Skipping conversion to type as not supported: {field.FieldType.Name}"); - continue; - } - } - field.SetValue(result, value); - } - else - { - logger.LogWarning($"Skipping field {key} when converting to {typeof(T).Name}"); - } - } - - Debug.WriteLine(result); - - return [result]; - } - - private List ParseArray(XElement type) - { - try - { - var result = new List(); - var data = type.Element("data"); - if (data != null) - { - foreach (var ele in data.Elements()) - { - result.AddRange(ParseValue(ele)); - } - } - - return [result.ToArray()]; // make an array; - } - catch (Exception) - { - logger.LogCritical($"Failed to Parse Array: {type}"); - throw; - } - } -} \ No newline at end of file diff --git a/src/Moonglade.Pingback/ClearPingbackCommand.cs b/src/Moonglade.Pingback/ClearPingbackCommand.cs index 0a7230e3a..564ce684f 100644 --- a/src/Moonglade.Pingback/ClearPingbackCommand.cs +++ b/src/Moonglade.Pingback/ClearPingbackCommand.cs @@ -1,12 +1,12 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; namespace Moonglade.Pingback; public record ClearPingbackCommand : IRequest; -public class ClearPingbackCommandHandler(IRepository repo) : IRequestHandler +public class ClearPingbackCommandHandler(MoongladeRepository repo) : IRequestHandler { public Task Handle(ClearPingbackCommand request, CancellationToken ct) => repo.Clear(ct); } \ No newline at end of file diff --git a/src/Moonglade.Pingback/DeletePingbackCommand.cs b/src/Moonglade.Pingback/DeletePingbackCommand.cs index a0942464c..4e6289907 100644 --- a/src/Moonglade.Pingback/DeletePingbackCommand.cs +++ b/src/Moonglade.Pingback/DeletePingbackCommand.cs @@ -1,12 +1,19 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; namespace Moonglade.Pingback; public record DeletePingbackCommand(Guid Id) : IRequest; -public class DeletePingbackCommandHandler(IRepository repo) : IRequestHandler +public class DeletePingbackCommandHandler(MoongladeRepository repo) : IRequestHandler { - public Task Handle(DeletePingbackCommand request, CancellationToken ct) => repo.DeleteAsync(request.Id, ct); + public async Task Handle(DeletePingbackCommand request, CancellationToken ct) + { + var pingback = await repo.GetByIdAsync(request.Id, ct); + if (pingback != null) + { + await repo.DeleteAsync(pingback, ct); + } + } } \ No newline at end of file diff --git a/src/Moonglade.Pingback/GetPingbacksQuery.cs b/src/Moonglade.Pingback/GetPingbacksQuery.cs index 9c81c83e1..368e3dd0c 100644 --- a/src/Moonglade.Pingback/GetPingbacksQuery.cs +++ b/src/Moonglade.Pingback/GetPingbacksQuery.cs @@ -1,12 +1,15 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; +using Moonglade.Data.Specifications; namespace Moonglade.Pingback; -public record GetPingbacksQuery : IRequest>; +public record GetPingbacksQuery : IRequest>; -public class GetPingbacksQueryHandler(IRepository repo) : IRequestHandler> +public class GetPingbacksQueryHandler(MoongladeRepository repo) : + IRequestHandler> { - public Task> Handle(GetPingbacksQuery request, CancellationToken ct) => repo.ListAsync(ct); + public Task> Handle(GetPingbacksQuery request, CancellationToken ct) => + repo.ListAsync(new PingbackReadOnlySpec(), ct); } \ No newline at end of file diff --git a/src/Moonglade.Pingback/Moonglade.Pingback.csproj b/src/Moonglade.Pingback/Moonglade.Pingback.csproj index 93c951c80..e06bd732e 100644 --- a/src/Moonglade.Pingback/Moonglade.Pingback.csproj +++ b/src/Moonglade.Pingback/Moonglade.Pingback.csproj @@ -14,5 +14,6 @@ + \ No newline at end of file diff --git a/src/Moonglade.Pingback/PingbackSender.cs b/src/Moonglade.Pingback/PingbackSender.cs index 2d8a49335..89b002b6a 100644 --- a/src/Moonglade.Pingback/PingbackSender.cs +++ b/src/Moonglade.Pingback/PingbackSender.cs @@ -1,11 +1,14 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moonglade.Utils; using System.Text.RegularExpressions; namespace Moonglade.Pingback; public class PingbackSender(HttpClient httpClient, IPingbackWebRequest pingbackWebRequest, - ILogger logger = null) + IConfiguration configuration, + ILogger logger) : IPingbackSender { public async Task TrySendPingAsync(string postUrl, string postContent) @@ -13,28 +16,41 @@ public async Task TrySendPingAsync(string postUrl, string postContent) try { var uri = new Uri(postUrl); + + if (!bool.Parse(configuration["AllowPingbackFromLocalhost"]!) && uri.IsLocalhostUrl()) + { + logger.LogWarning("Source URL is localhost, skipping."); + return; + } + var content = postContent.ToUpperInvariant(); if (content.Contains("HTTP://") || content.Contains("HTTPS://")) { - logger?.LogInformation("URL is detected in post content, trying to send ping requests."); + logger.LogInformation("URL is detected in post content, trying to send ping requests."); foreach (var url in GetUrlsFromContent(postContent)) { - logger?.LogInformation("Pinging URL: " + url); + if (!bool.Parse(configuration["AllowPingbackToLocalhost"]!) && url.IsLocalhostUrl()) + { + logger.LogWarning("Target URL is localhost, skipping."); + continue; + } + + logger.LogInformation("Pinging URL: " + url); try { await SendAsync(uri, url); } catch (Exception e) { - logger?.LogError(e, "SendAsync Ping Error."); + logger.LogError(e, "SendAsync Ping Error."); } } } } catch (Exception ex) { - logger?.LogError(ex, $"{nameof(TrySendPingAsync)}({postUrl})"); + logger.LogError(ex, $"{nameof(TrySendPingAsync)}({postUrl})"); } } @@ -54,14 +70,14 @@ private async Task SendAsync(Uri sourceUrl, Uri targetUrl) if (key is null || value is null) { - logger?.LogInformation($"Pingback endpoint is not found for URL '{targetUrl}', ping request is terminated."); + logger.LogInformation($"Pingback endpoint is not found for URL '{targetUrl}', ping request is terminated."); return; } var pingUrl = value.FirstOrDefault(); if (pingUrl is not null) { - logger?.LogInformation($"Found Ping service URL '{pingUrl}' on target '{sourceUrl}'"); + logger.LogInformation($"Found Ping service URL '{pingUrl}' on target '{sourceUrl}'"); bool successUrlCreation = Uri.TryCreate(pingUrl, UriKind.Absolute, out var url); if (successUrlCreation) @@ -70,13 +86,13 @@ private async Task SendAsync(Uri sourceUrl, Uri targetUrl) } else { - logger?.LogInformation($"Invliad Ping service URL '{pingUrl}'"); + logger.LogInformation($"Invliad Ping service URL '{pingUrl}'"); } } } catch (Exception e) { - logger?.LogError(e, $"{nameof(SendAsync)}({sourceUrl},{targetUrl})"); + logger.LogError(e, $"{nameof(SendAsync)}({sourceUrl},{targetUrl})"); } } diff --git a/src/Moonglade.Pingback/ReceivePingCommand.cs b/src/Moonglade.Pingback/ReceivePingCommand.cs index 38b87d20e..b6f83571a 100644 --- a/src/Moonglade.Pingback/ReceivePingCommand.cs +++ b/src/Moonglade.Pingback/ReceivePingCommand.cs @@ -1,8 +1,8 @@ using MediatR; using Microsoft.Extensions.Logging; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; -using Moonglade.Data.Spec; +using Moonglade.Data.Specifications; using System.Text.RegularExpressions; using System.Xml; @@ -21,8 +21,8 @@ public class ReceivePingCommand(string requestBody, string ip, Action logger, IPingSourceInspector pingSourceInspector, - IRepository pingbackRepo, - IRepository postRepo) : IRequestHandler + MoongladeRepository pingbackRepo, + MoongladeRepository postRepo) : IRequestHandler { private string _sourceUrl; private string _targetUrl; @@ -56,8 +56,8 @@ public async Task Handle(ReceivePingCommand request, Cancellat } var (slug, pubDate) = GetSlugInfoFromUrl(pingRequest.TargetUrl); - var spec = new PostSpec(pubDate, slug); - var (id, title) = await postRepo.FirstOrDefaultAsync(spec, p => new Tuple(p.Id, p.Title)); + var spec = new PostByDateAndSlugForIdTitleSpec(pubDate, slug); + var (id, title) = await postRepo.FirstOrDefaultAsync(spec, ct); if (id == Guid.Empty) { logger.LogError($"Can not get post id and title for url '{pingRequest.TargetUrl}'"); @@ -66,11 +66,7 @@ public async Task Handle(ReceivePingCommand request, Cancellat logger.LogInformation($"Post '{id}:{title}' is found for ping."); - var pinged = await pingbackRepo.AnyAsync(p => - p.TargetPostId == id && - p.SourceUrl == pingRequest.SourceUrl && - p.SourceIp.Trim() == request.IP, ct); - + var pinged = await pingbackRepo.AnyAsync(new PingbackSpec(id, pingRequest.SourceUrl, request.IP), ct); if (pinged) return PingbackResponse.Error48PingbackAlreadyRegistered; logger.LogInformation("Adding received pingback..."); diff --git a/src/Moonglade.Syndication/GetAtomStringQuery.cs b/src/Moonglade.Syndication/GetAtomStringQuery.cs index ac3baef54..aea7afeac 100644 --- a/src/Moonglade.Syndication/GetAtomStringQuery.cs +++ b/src/Moonglade.Syndication/GetAtomStringQuery.cs @@ -5,7 +5,7 @@ namespace Moonglade.Syndication; -public record GetAtomStringQuery(string CategoryName = null) : IRequest; +public record GetAtomStringQuery(string Slug = null) : IRequest; public class GetAtomStringQueryHandler : IRequestHandler { @@ -31,7 +31,7 @@ public GetAtomStringQueryHandler(IBlogConfig blogConfig, ISyndicationDataSource public async Task Handle(GetAtomStringQuery request, CancellationToken ct) { - var data = await _sdds.GetFeedDataAsync(request.CategoryName); + var data = await _sdds.GetFeedDataAsync(request.Slug); if (data is null) return null; _feedGenerator.FeedItemCollection = data; diff --git a/src/Moonglade.Syndication/SyndicationDataSource.cs b/src/Moonglade.Syndication/SyndicationDataSource.cs index 984d561b1..c971fccbf 100644 --- a/src/Moonglade.Syndication/SyndicationDataSource.cs +++ b/src/Moonglade.Syndication/SyndicationDataSource.cs @@ -1,31 +1,31 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Moonglade.Configuration; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; -using Moonglade.Data.Spec; +using Moonglade.Data.Specifications; using Moonglade.Utils; namespace Moonglade.Syndication; public interface ISyndicationDataSource { - Task> GetFeedDataAsync(string catRoute = null); + Task> GetFeedDataAsync(string catSlug = null); } public class SyndicationDataSource : ISyndicationDataSource { private readonly string _baseUrl; private readonly IBlogConfig _blogConfig; - private readonly IRepository _catRepo; - private readonly IRepository _postRepo; + private readonly MoongladeRepository _catRepo; + private readonly MoongladeRepository _postRepo; private readonly IConfiguration _configuration; public SyndicationDataSource( IBlogConfig blogConfig, IHttpContextAccessor httpContextAccessor, - IRepository catRepo, - IRepository postRepo, + MoongladeRepository catRepo, + MoongladeRepository postRepo, IConfiguration configuration) { _blogConfig = blogConfig; @@ -37,12 +37,12 @@ public SyndicationDataSource( _baseUrl = $"{acc.HttpContext.Request.Scheme}://{acc.HttpContext.Request.Host}"; } - public async Task> GetFeedDataAsync(string catRoute = null) + public async Task> GetFeedDataAsync(string catSlug = null) { - IReadOnlyList itemCollection; - if (!string.IsNullOrWhiteSpace(catRoute)) + List itemCollection; + if (!string.IsNullOrWhiteSpace(catSlug)) { - var cat = await _catRepo.GetAsync(c => c.RouteName == catRoute); + var cat = await _catRepo.FirstOrDefaultAsync(new CategoryBySlugSpec(catSlug)); if (cat is null) return null; itemCollection = await GetFeedEntriesAsync(cat.Id); @@ -55,15 +55,15 @@ public async Task> GetFeedDataAsync(string catRoute = n return itemCollection; } - private async Task> GetFeedEntriesAsync(Guid? catId = null) + private async Task> GetFeedEntriesAsync(Guid? catId = null) { int? top = null; - if (_blogConfig.FeedSettings.RssItemCount != 0) + if (_blogConfig.FeedSettings.FeedItemCount != 0) { - top = _blogConfig.FeedSettings.RssItemCount; + top = _blogConfig.FeedSettings.FeedItemCount; } - var postSpec = new PostSpec(catId, top); + var postSpec = new PostByCatSpec(catId, top); var list = await _postRepo.SelectAsync(postSpec, p => p.PubDateUtc != null ? new FeedEntry { Id = p.Id.ToString(), diff --git a/src/Moonglade.Theme/CreateThemeCommand.cs b/src/Moonglade.Theme/CreateThemeCommand.cs index 22f71962d..4dd8284ce 100644 --- a/src/Moonglade.Theme/CreateThemeCommand.cs +++ b/src/Moonglade.Theme/CreateThemeCommand.cs @@ -1,18 +1,19 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; +using Moonglade.Data.Specifications; using System.Text.Json; namespace Moonglade.Theme; public record CreateThemeCommand(string Name, IDictionary Rules) : IRequest; -public class CreateThemeCommandHandler(IRepository repo) : IRequestHandler +public class CreateThemeCommandHandler(MoongladeRepository repo) : IRequestHandler { public async Task Handle(CreateThemeCommand request, CancellationToken ct) { var (name, dictionary) = request; - if (await repo.AnyAsync(p => p.ThemeName == name.Trim(), ct)) return 0; + if (await repo.AnyAsync(new ThemeByNameSpec(name.Trim()), ct)) return -1; var rules = JsonSerializer.Serialize(dictionary); var entity = new BlogThemeEntity diff --git a/src/Moonglade.Theme/DeleteThemeCommand.cs b/src/Moonglade.Theme/DeleteThemeCommand.cs index b8f253e2b..4e5afa869 100644 --- a/src/Moonglade.Theme/DeleteThemeCommand.cs +++ b/src/Moonglade.Theme/DeleteThemeCommand.cs @@ -1,21 +1,20 @@ using MediatR; using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; namespace Moonglade.Theme; public record DeleteThemeCommand(int Id) : IRequest; -public class DeleteThemeCommandHandler(IRepository repo) : IRequestHandler +public class DeleteThemeCommandHandler(MoongladeRepository repo) : IRequestHandler { public async Task Handle(DeleteThemeCommand request, CancellationToken ct) { - var theme = await repo.GetAsync(request.Id, ct); + var theme = await repo.GetByIdAsync(request.Id, ct); if (null == theme) return OperationCode.ObjectNotFound; if (theme.ThemeType == ThemeType.System) return OperationCode.Canceled; - await repo.DeleteAsync(request.Id, ct); + await repo.DeleteAsync(theme, ct); return OperationCode.Done; } } \ No newline at end of file diff --git a/src/Moonglade.Theme/GetAllThemeSegmentQuery.cs b/src/Moonglade.Theme/GetAllThemeSegmentQuery.cs index 85f373ffa..c61a8848c 100644 --- a/src/Moonglade.Theme/GetAllThemeSegmentQuery.cs +++ b/src/Moonglade.Theme/GetAllThemeSegmentQuery.cs @@ -1,19 +1,14 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; +using Moonglade.Data.Specifications; namespace Moonglade.Theme; -public record GetAllThemeSegmentQuery : IRequest>; +public record GetAllThemeSegmentQuery : IRequest>; -public class GetAllThemeSegmentQueryHandler(IRepository repo) : IRequestHandler> +public class GetAllThemeSegmentQueryHandler(MoongladeRepository repo) : IRequestHandler> { - public Task> Handle(GetAllThemeSegmentQuery request, CancellationToken ct) - { - return repo.SelectAsync(p => new ThemeSegment - { - Id = p.Id, - Name = p.ThemeName - }, ct); - } + public Task> Handle(GetAllThemeSegmentQuery request, CancellationToken ct) => + repo.ListAsync(new BlogThemeForIdNameSpec(), ct); } \ No newline at end of file diff --git a/src/Moonglade.Theme/GetSiteThemeStyleSheetQuery.cs b/src/Moonglade.Theme/GetSiteThemeStyleSheetQuery.cs index b0018cd20..fd34cd5e0 100644 --- a/src/Moonglade.Theme/GetSiteThemeStyleSheetQuery.cs +++ b/src/Moonglade.Theme/GetSiteThemeStyleSheetQuery.cs @@ -1,17 +1,17 @@ using MediatR; +using Moonglade.Data; using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; using System.Text; using System.Text.Json; namespace Moonglade.Theme; public record GetSiteThemeStyleSheetQuery(int Id) : IRequest; -public class GetStyleSheetQueryHandler(IRepository repo) : IRequestHandler +public class GetStyleSheetQueryHandler(MoongladeRepository repo) : IRequestHandler { public async Task Handle(GetSiteThemeStyleSheetQuery request, CancellationToken ct) { - var theme = await repo.GetAsync(request.Id, ct); + var theme = await repo.GetByIdAsync(request.Id, ct); if (null == theme) return null; if (string.IsNullOrWhiteSpace(theme.CssRules)) diff --git a/src/Moonglade.Theme/ThemeSegment.cs b/src/Moonglade.Theme/ThemeSegment.cs deleted file mode 100644 index c3037869f..000000000 --- a/src/Moonglade.Theme/ThemeSegment.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Moonglade.Theme; - -public record ThemeSegment -{ - public int Id { get; set; } - public string Name { get; set; } -} \ No newline at end of file diff --git a/src/Moonglade.Utils/ContentProcessor.cs b/src/Moonglade.Utils/ContentProcessor.cs index 0e30fd6cb..883184db1 100644 --- a/src/Moonglade.Utils/ContentProcessor.cs +++ b/src/Moonglade.Utils/ContentProcessor.cs @@ -1,6 +1,6 @@ using Markdig; -using NUglify; using System.Text.RegularExpressions; +using System.Xml.Linq; namespace Moonglade.Utils; @@ -37,11 +37,17 @@ public static string RemoveTags(string html) return string.Empty; } - var result = Uglify.HtmlToText(html); + try + { + var doc = XDocument.Parse(html); + var result = doc.Root?.DescendantNodes().OfType().Aggregate("", (current, node) => current + node); - return !result.HasErrors && !string.IsNullOrWhiteSpace(result.Code) - ? result.Code.Trim() - : RemoveTagsBackup(html); + return result?.Trim(); + } + catch (Exception) + { + return RemoveTagsBackup(html); + } } public static string Ellipsize(this string text, int characterCount) diff --git a/src/Moonglade.Utils/Helper.cs b/src/Moonglade.Utils/Helper.cs index 70a7a78c1..28f492cfb 100644 --- a/src/Moonglade.Utils/Helper.cs +++ b/src/Moonglade.Utils/Helper.cs @@ -41,6 +41,16 @@ public static string AppVersion } } + public static bool IsRunningOnAzureAppService() + { + return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME")); + } + + public static bool IsRunningInDocker() + { + return Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; + } + public static string GetClientIP(HttpContext context) => context?.Connection.RemoteIpAddress?.ToString(); public static int ComputeCheckSum(string input) @@ -122,19 +132,9 @@ public static string GetMd5Hash(string input) return sBuilder.ToString(); } - public static string HashPassword(string plainMessage) - { - if (string.IsNullOrWhiteSpace(plainMessage)) return string.Empty; - - var data = Encoding.UTF8.GetBytes(plainMessage); - using var sha = SHA256.Create(); - sha.TransformFinalBlock(data, 0, data.Length); - return Convert.ToBase64String(sha.Hash ?? throw new InvalidOperationException()); - } - // https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing?view=aspnetcore-6.0 // This is not secure, but better than nothing. - public static string HashPassword2(string clearPassword, string saltBase64) + public static string HashPassword(string clearPassword, string saltBase64) { var salt = Convert.FromBase64String(saltBase64); @@ -359,23 +359,22 @@ private static Regex CreateEmailRegEx() return new(pattern, options); } - public static string GenerateSlug(this string phrase) + public static bool IsValidTagName(string tagDisplayName) { - string str = phrase.RemoveAccent().ToLower(); - // invalid chars - str = Regex.Replace(str, @"[^a-z0-9\s-]", ""); - // convert multiple spaces into one space - str = Regex.Replace(str, @"\s+", " ").Trim(); - // cut and trim - str = str[..(str.Length <= 45 ? str.Length : 45)].Trim(); - str = Regex.Replace(str, @"\s", "-"); // hyphens - return str; - } + if (string.IsNullOrWhiteSpace(tagDisplayName)) return false; - public static string RemoveAccent(this string txt) - { - byte[] bytes = Encoding.GetEncoding("Cyrillic").GetBytes(txt); - return Encoding.ASCII.GetString(bytes); + // Regex performance best practice + // See https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices + + const string pattern = @"^[a-zA-Z 0-9\.\-\+\#\s]*$"; + var isEng = Regex.IsMatch(tagDisplayName, pattern); + if (isEng) return true; + + // https://docs.microsoft.com/en-us/dotnet/standard/base-types/character-classes-in-regular-expressions#supported-named-blocks + const string chsPattern = @"\p{IsCJKUnifiedIdeographs}"; + var isChs = Regex.IsMatch(tagDisplayName, chsPattern); + + return isChs; } public static string CombineErrorMessages(this ModelStateDictionary modelStateDictionary, string sep = ", ") @@ -418,6 +417,32 @@ public static void ValidatePagingParameters(int pageSize, int pageIndex) { "+", "-plus" } }; + public static string NormalizeName(string orgTagName, IDictionary normalizations) + { + var isEnglishName = Regex.IsMatch(orgTagName, @"^[a-zA-Z 0-9\.\-\+\#\s]*$"); + if (isEnglishName) + { + // special case + if (orgTagName.Equals(".net", StringComparison.OrdinalIgnoreCase)) + { + return "dot-net"; + } + + var result = new StringBuilder(orgTagName); + foreach (var (key, value) in normalizations) + { + result.Replace(key, value); + } + return result.ToString().ToLower(); + } + + var bytes = Encoding.Unicode.GetBytes(orgTagName); + var hexArray = bytes.Select(b => $"{b:x2}"); + var hexName = string.Join('-', hexArray); + + return hexName; + } + public static bool IsValidHeaderName(string headerName) { if (string.IsNullOrEmpty(headerName)) diff --git a/src/Moonglade.Utils/Moonglade.Utils.csproj b/src/Moonglade.Utils/Moonglade.Utils.csproj index 7a70324e0..4fcc9def2 100644 --- a/src/Moonglade.Utils/Moonglade.Utils.csproj +++ b/src/Moonglade.Utils/Moonglade.Utils.csproj @@ -10,7 +10,6 @@ - - + \ No newline at end of file diff --git a/src/Moonglade.Utils/UrlExtension.cs b/src/Moonglade.Utils/UrlExtension.cs index 7b7f4a1c0..be5bb1331 100644 --- a/src/Moonglade.Utils/UrlExtension.cs +++ b/src/Moonglade.Utils/UrlExtension.cs @@ -1,4 +1,6 @@ -namespace Moonglade.Utils; +using System.Net; + +namespace Moonglade.Utils; public static class UrlExtension { @@ -36,4 +38,31 @@ public static string CombineUrl(this string url, string path) return url.TrimEnd('/') + "/" + path.TrimStart('/'); } + + public static bool IsLocalhostUrl(this Uri uri) + { + try + { + if (uri.IsLoopback) + { + // localhost, 127.0.0.1, [::1] + return true; + } + + // Get the local host name and compare it with the URL host + string localHostName = Dns.GetHostName(); + if (uri.Host.Equals(localHostName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Get local IP addresses and compare them with the URL host + IPAddress[] localIPs = Dns.GetHostAddresses(Dns.GetHostName()); + return localIPs.Any(addr => uri.Host.Equals(addr.ToString())); + } + catch (UriFormatException) + { + return false; + } + } } \ No newline at end of file diff --git a/src/Moonglade.Web/Configuration/ConfigureEndpoints.cs b/src/Moonglade.Web/Configuration/ConfigureEndpoints.cs index c2c453103..a4af2d815 100644 --- a/src/Moonglade.Web/Configuration/ConfigureEndpoints.cs +++ b/src/Moonglade.Web/Configuration/ConfigureEndpoints.cs @@ -11,7 +11,7 @@ public static Task WriteResponse(HttpContext context, HealthReport result) Helper.AppVersion, DotNetVersion = Environment.Version.ToString(), EnvironmentTags = Helper.GetEnvironmentTags(), - GeoMatch = context.Request.Headers["x-afd-geo-match"] + GeoMatch = context.Request.Headers["x-geo-match"] }; return context.Response.WriteAsJsonAsync(obj); diff --git a/src/Moonglade.Web/Controllers/AssetsController.cs b/src/Moonglade.Web/Controllers/AssetsController.cs index b8ce2e3b4..eb0cc3690 100644 --- a/src/Moonglade.Web/Controllers/AssetsController.cs +++ b/src/Moonglade.Web/Controllers/AssetsController.cs @@ -35,7 +35,7 @@ public async Task Avatar(ICacheAside cache) [HttpPost("avatar")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCachePartition.General, "avatar" })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCachePartition.General, "avatar"])] public async Task Avatar([FromBody] string base64Img) { base64Img = base64Img.Trim(); diff --git a/src/Moonglade.Web/Controllers/CategoryController.cs b/src/Moonglade.Web/Controllers/CategoryController.cs index 9aa2f0e4b..98d8a58ed 100644 --- a/src/Moonglade.Web/Controllers/CategoryController.cs +++ b/src/Moonglade.Web/Controllers/CategoryController.cs @@ -1,5 +1,8 @@ using Moonglade.Core.CategoryFeature; +using Moonglade.Data.Entities; +using Moonglade.Data.Exporting; using Moonglade.Web.Attributes; +using System.Text.Json; namespace Moonglade.Web.Controllers; @@ -9,18 +12,23 @@ namespace Moonglade.Web.Controllers; public class CategoryController(IMediator mediator) : ControllerBase { [HttpGet("{id:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Get([NotEmpty] Guid id) { var cat = await mediator.Send(new GetCategoryQuery(id)); if (null == cat) return NotFound(); - return Ok(cat); + // return Ok(cat); + + // Workaround .NET by design bug: https://stackoverflow.com/questions/60184661/net-core-3-jsonignore-not-working-when-requesting-single-resource + // https://github.com/dotnet/aspnetcore/issues/31396 + // https://github.com/dotnet/efcore/issues/33223 + return Content(JsonSerializer.Serialize(cat, MoongladeJsonSerializerOptions.Default), "application/json"); } [HttpGet("list")] - [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType>(StatusCodes.Status200OK)] public async Task List() { var list = await mediator.Send(new GetCategoriesQuery()); diff --git a/src/Moonglade.Web/Controllers/CommentController.cs b/src/Moonglade.Web/Controllers/CommentController.cs index 46ddc250e..26bbaa3bf 100644 --- a/src/Moonglade.Web/Controllers/CommentController.cs +++ b/src/Moonglade.Web/Controllers/CommentController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; +using Moonglade.Comments.Moderator; using Moonglade.Email.Client; using Moonglade.Web.Attributes; using System.ComponentModel.DataAnnotations; @@ -11,14 +12,15 @@ namespace Moonglade.Web.Controllers; [CommentProviderGate] public class CommentController( IMediator mediator, + IModeratorService moderator, IBlogConfig blogConfig, ILogger logger) : ControllerBase { [HttpPost("{postId:guid}")] [AllowAnonymous] [ServiceFilter(typeof(ValidateCaptcha))] - [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status409Conflict)] @@ -32,17 +34,32 @@ public async Task Create([NotEmpty] Guid postId, CommentRequest r if (!blogConfig.ContentSettings.EnableComments) return Forbid(); + if (blogConfig.ContentSettings.EnableWordFilter) + { + switch (blogConfig.ContentSettings.WordFilterMode) + { + case WordFilterMode.Mask: + request.Username = await moderator.Mask(request.Username); + request.Content = await moderator.Mask(request.Content); + break; + case WordFilterMode.Block: + if (await moderator.Detect(request.Username, request.Content)) + { + await Task.CompletedTask; + ModelState.AddModelError(nameof(request.Content), "Your comment contains bad bad word."); + return Conflict(ModelState); + } + break; + } + } + var ip = (bool)HttpContext.Items["DNT"]! ? "N/A" : Helper.GetClientIP(HttpContext); var item = await mediator.Send(new CreateCommentCommand(postId, request, ip)); - switch (item.Status) + if (null == item) { - case -1: - ModelState.AddModelError(nameof(request.Content), "Your comment contains bad bad word."); - return Conflict(ModelState); - case -2: - ModelState.AddModelError(nameof(postId), "Comment is closed for this post."); - return Conflict(ModelState); + ModelState.AddModelError(nameof(postId), "Comment is closed for this post."); + return Conflict(ModelState); } if (blogConfig.NotificationSettings.SendEmailOnNewComment) @@ -50,11 +67,11 @@ public async Task Create([NotEmpty] Guid postId, CommentRequest r try { await mediator.Publish(new CommentNotification( - item.Item.Username, - item.Item.Email, - item.Item.IpAddress, - item.Item.PostTitle, - item.Item.CommentContent)); + item.Username, + item.Email, + item.IpAddress, + item.PostTitle, + item.CommentContent)); } catch (Exception e) { @@ -67,14 +84,14 @@ await mediator.Publish(new CommentNotification( return Created("moonglade://empty", item); } - return Ok(); + return NoContent(); } [HttpPut("{commentId:guid}/approval/toggle")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task Approval([NotEmpty] Guid commentId) { - await mediator.Send(new ToggleApprovalCommand(new[] { commentId })); + await mediator.Send(new ToggleApprovalCommand([commentId])); return Ok(commentId); } diff --git a/src/Moonglade.Web/Controllers/DataPortingController.cs b/src/Moonglade.Web/Controllers/DataPortingController.cs index d6995d37d..abface033 100644 --- a/src/Moonglade.Web/Controllers/DataPortingController.cs +++ b/src/Moonglade.Web/Controllers/DataPortingController.cs @@ -18,11 +18,6 @@ public async Task ExportDownload(ExportType type, CancellationTok _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) }; - return exportResult.ExportFormat switch - { - ExportFormat.ZippedJsonFiles => PhysicalFile(exportResult.FilePath, exportResult.ContentType, - Path.GetFileName(exportResult.FilePath)), - _ => BadRequest(ModelState.CombineErrorMessages()) - }; + return PhysicalFile(exportResult.FilePath, "application/zip", Path.GetFileName(exportResult.FilePath)); } } \ No newline at end of file diff --git a/src/Moonglade.Web/Controllers/FriendLinkController.cs b/src/Moonglade.Web/Controllers/FriendLinkController.cs index 34451ae99..0749a6d58 100644 --- a/src/Moonglade.Web/Controllers/FriendLinkController.cs +++ b/src/Moonglade.Web/Controllers/FriendLinkController.cs @@ -11,10 +11,10 @@ public class FriendLinkController(IMediator mediator) : ControllerBase { [HttpPost] [ProducesResponseType(StatusCodes.Status201Created)] - public async Task Create(AddLinkCommand command) + public async Task Create(EditLinkRequest request) { - await mediator.Send(command); - return Created(new Uri(command.LinkUrl), command); + await mediator.Send(new AddLinkCommand(request)); + return Created(new Uri(request.LinkUrl), request); } [HttpGet("{id:guid}")] @@ -38,10 +38,9 @@ public async Task List() [HttpPut("{id:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task Update([NotEmpty] Guid id, UpdateLinkCommand command) + public async Task Update([NotEmpty] Guid id, EditLinkRequest request) { - command.Id = id; - await mediator.Send(command); + await mediator.Send(new UpdateLinkCommand(id, request)); return NoContent(); } diff --git a/src/Moonglade.Web/Controllers/LocalAccountController.cs b/src/Moonglade.Web/Controllers/LocalAccountController.cs deleted file mode 100644 index ba900b590..000000000 --- a/src/Moonglade.Web/Controllers/LocalAccountController.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Moonglade.Web.Attributes; -using System.ComponentModel.DataAnnotations; -using System.Text.RegularExpressions; - -namespace Moonglade.Web.Controllers; - -[Authorize] -[ApiController] -[Route("api/[controller]")] -public class LocalAccountController(IMediator mediator) : ControllerBase -{ - [HttpPost] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task Create(CreateAccountCommand command) - { - if (await mediator.Send(new AccountExistsQuery(command.Username))) - { - ModelState.AddModelError("username", $"User '{command.Username}' already exist."); - return Conflict(ModelState); - } - - await mediator.Send(command); - return Ok(); - } - - [HttpDelete("{id:guid}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task Delete([NotEmpty] Guid id) - { - var uidClaim = User.Claims.FirstOrDefault(c => c.Type == "uid"); - if (null == uidClaim || string.IsNullOrWhiteSpace(uidClaim.Value)) - { - return StatusCode(StatusCodes.Status500InternalServerError, "Can not get current uid."); - } - - if (id.ToString() == uidClaim.Value) - { - return Conflict("Can not delete current user."); - } - - var count = await mediator.Send(new CountAccountsQuery()); - if (count == 1) - { - return Conflict("Can not delete last account."); - } - - await mediator.Send(new DeleteAccountCommand(id)); - return NoContent(); - } - - [HttpPut("{id:guid}/password")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task ResetPassword([NotEmpty] Guid id, [FromBody][Required] string newPassword) - { - if (!Regex.IsMatch(newPassword, @"^(?=.*[A-Za-z])(?=.*\d)[!@#$%^&*A-Za-z\d]{8,}$")) - { - return Conflict("Password must be minimum eight characters, at least one letter and one number"); - } - - await mediator.Send(new ChangePasswordCommand(id, newPassword)); - return NoContent(); - } -} \ No newline at end of file diff --git a/src/Moonglade.Web/Controllers/PageController.cs b/src/Moonglade.Web/Controllers/PageController.cs index 556e4b0d4..73cfa99af 100644 --- a/src/Moonglade.Web/Controllers/PageController.cs +++ b/src/Moonglade.Web/Controllers/PageController.cs @@ -1,6 +1,5 @@ using Moonglade.Core.PageFeature; using Moonglade.Web.Attributes; -using NUglify; namespace Moonglade.Web.Controllers; @@ -10,33 +9,22 @@ namespace Moonglade.Web.Controllers; public class PageController(ICacheAside cache, IMediator mediator) : Controller { [HttpPost] - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCacheType.SiteMap })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCacheType.SiteMap])] [ProducesResponseType(StatusCodes.Status200OK)] - public Task Create(EditPageRequest model) => - CreateOrEdit(model, async request => await mediator.Send(new CreatePageCommand(request))); + public async Task Create(EditPageRequest model) + { + var uid = await mediator.Send(new CreatePageCommand(model)); + + cache.Remove(BlogCachePartition.Page.ToString(), model.Slug.ToLower()); + return Ok(new { PageId = uid }); + } [HttpPut("{id:guid}")] - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCacheType.SiteMap })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCacheType.SiteMap])] [ProducesResponseType(StatusCodes.Status200OK)] - public Task Edit([NotEmpty] Guid id, EditPageRequest model) => - CreateOrEdit(model, async request => await mediator.Send(new UpdatePageCommand(id, request))); - - private async Task CreateOrEdit(EditPageRequest model, Func> pageServiceAction) + public async Task Edit([NotEmpty] Guid id, EditPageRequest model) { - if (!string.IsNullOrWhiteSpace(model.CssContent)) - { - var uglifyTest = Uglify.Css(model.CssContent); - if (uglifyTest.HasErrors) - { - foreach (var err in uglifyTest.Errors) - { - ModelState.AddModelError(model.CssContent, err.ToString()); - } - return BadRequest(ModelState.CombineErrorMessages()); - } - } - - var uid = await pageServiceAction(model); + var uid = await mediator.Send(new UpdatePageCommand(id, model)); cache.Remove(BlogCachePartition.Page.ToString(), model.Slug.ToLower()); return Ok(new { PageId = uid }); diff --git a/src/Moonglade.Web/Controllers/PostController.cs b/src/Moonglade.Web/Controllers/PostController.cs index da375889e..039281507 100644 --- a/src/Moonglade.Web/Controllers/PostController.cs +++ b/src/Moonglade.Web/Controllers/PostController.cs @@ -16,12 +16,12 @@ public class PostController( ILogger logger) : ControllerBase { [HttpPost("createoredit")] - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] - { + [TypeFilter(typeof(ClearBlogCache), Arguments = + [ BlogCacheType.SiteMap | BlogCacheType.Subscription | BlogCacheType.PagingCount - })] + ])] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task CreateOrEdit(PostEditModel model, LinkGenerator linkGenerator) @@ -73,12 +73,12 @@ await mediator.Send(new CreatePostCommand(model)) : } } - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] - { + [TypeFilter(typeof(ClearBlogCache), Arguments = + [ BlogCacheType.SiteMap | BlogCacheType.Subscription | BlogCacheType.PagingCount - })] + ])] [HttpPost("{postId:guid}/restore")] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Restore([NotEmpty] Guid postId) @@ -87,12 +87,12 @@ public async Task Restore([NotEmpty] Guid postId) return NoContent(); } - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] - { + [TypeFilter(typeof(ClearBlogCache), Arguments = + [ BlogCacheType.SiteMap | BlogCacheType.Subscription | BlogCacheType.PagingCount - })] + ])] [HttpDelete("{postId:guid}/recycle")] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Delete([NotEmpty] Guid postId) @@ -101,7 +101,7 @@ public async Task Delete([NotEmpty] Guid postId) return NoContent(); } - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCacheType.Subscription | BlogCacheType.SiteMap })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCacheType.Subscription | BlogCacheType.SiteMap])] [HttpDelete("{postId:guid}/destroy")] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DeleteFromRecycleBin([NotEmpty] Guid postId) @@ -110,7 +110,7 @@ public async Task DeleteFromRecycleBin([NotEmpty] Guid postId) return NoContent(); } - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCacheType.Subscription | BlogCacheType.SiteMap })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCacheType.Subscription | BlogCacheType.SiteMap])] [HttpDelete("recyclebin")] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task EmptyRecycleBin() diff --git a/src/Moonglade.Web/Controllers/SettingsController.cs b/src/Moonglade.Web/Controllers/SettingsController.cs index e544a2435..2e35972ae 100644 --- a/src/Moonglade.Web/Controllers/SettingsController.cs +++ b/src/Moonglade.Web/Controllers/SettingsController.cs @@ -1,7 +1,6 @@ using Edi.PasswordGenerator; using Microsoft.AspNetCore.Localization; using Moonglade.Email.Client; -using NUglify; namespace Moonglade.Web.Controllers; @@ -40,7 +39,7 @@ public IActionResult SetLanguage(string culture, string returnUrl) [HttpPost("general")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCachePartition.General, "theme" })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCachePartition.General, "theme"])] public async Task General(GeneralSettings model, ITimeZoneResolver timeZoneResolver) { model.AvatarUrl = blogConfig.GeneralSettings.AvatarUrl; @@ -148,25 +147,12 @@ public async Task Image(ImageSettings model, IBlogImageStorage im [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Advanced(AdvancedSettings model) { - model.MetaWeblogPasswordHash = !string.IsNullOrWhiteSpace(model.MetaWeblogPassword) ? - Helper.HashPassword(model.MetaWeblogPassword) : - blogConfig.AdvancedSettings.MetaWeblogPasswordHash; - blogConfig.AdvancedSettings = model; await SaveConfigAsync(blogConfig.AdvancedSettings); return NoContent(); } - [HttpPost("shutdown")] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public IActionResult Shutdown(IHostApplicationLifetime applicationLifetime) - { - logger.LogWarning($"Shutdown is requested by '{User.Identity?.Name}'."); - applicationLifetime.StopApplication(); - return Accepted(); - } - [HttpPost("reset")] [ProducesResponseType(StatusCodes.Status202Accepted)] public async Task Reset(BlogDbContext context, IHostApplicationLifetime applicationLifetime) @@ -190,16 +176,6 @@ public async Task CustomStyleSheet(CustomStyleSheetSettings model return BadRequest(ModelState.CombineErrorMessages()); } - var uglifyTest = Uglify.Css(model.CssCode); - if (uglifyTest.HasErrors) - { - foreach (var err in uglifyTest.Errors) - { - ModelState.AddModelError(model.CssCode, err.ToString()); - } - return BadRequest(ModelState.CombineErrorMessages()); - } - blogConfig.CustomStyleSheetSettings = model; await SaveConfigAsync(blogConfig.CustomStyleSheetSettings); @@ -239,6 +215,24 @@ public IActionResult GeneratePassword([FromServices] IPasswordGenerator password }); } + [HttpPut("password/local")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task UpdateLocalAccountPassword(UpdateLocalAccountPasswordRequest request) + { + var oldPasswordValid = blogConfig.LocalAccountSettings.PasswordHash == Helper.HashPassword(request.OldPassword.Trim(), blogConfig.LocalAccountSettings.PasswordSalt); + + if (!oldPasswordValid) return Conflict("Old password is incorrect."); + + var newSalt = Helper.GenerateSalt(); + blogConfig.LocalAccountSettings.Username = request.NewUsername.Trim(); + blogConfig.LocalAccountSettings.PasswordSalt = newSalt; + blogConfig.LocalAccountSettings.PasswordHash = Helper.HashPassword(request.NewPassword, newSalt); + + await SaveConfigAsync(blogConfig.LocalAccountSettings); + return NoContent(); + } + private async Task SaveConfigAsync(T blogSettings) where T : IBlogSettings { var kvp = blogConfig.UpdateAsync(blogSettings); diff --git a/src/Moonglade.Web/Controllers/SubscriptionController.cs b/src/Moonglade.Web/Controllers/SubscriptionController.cs index df95965ed..c5097ef14 100644 --- a/src/Moonglade.Web/Controllers/SubscriptionController.cs +++ b/src/Moonglade.Web/Controllers/SubscriptionController.cs @@ -16,7 +16,7 @@ public async Task Opml() if (!blogConfig.AdvancedSettings.EnableOpml) return NotFound(); var cats = await mediator.Send(new GetCategoriesQuery()); - var catInfos = cats.Select(c => new KeyValuePair(c.DisplayName, c.RouteName)); + var catInfos = cats.Select(c => new KeyValuePair(c.DisplayName, c.Slug)); var rootUrl = Helper.ResolveRootUrl(HttpContext, blogConfig.GeneralSettings.CanonicalPrefix); var oi = new OpmlDoc @@ -33,18 +33,18 @@ public async Task Opml() return Content(xml, "text/xml"); } - [HttpGet("rss/{routeName?}")] - public async Task Rss([MaxLength(64)] string routeName = null) + [HttpGet("rss/{slug?}")] + public async Task Rss([MaxLength(64)] string slug = null) { - bool hasRoute = !string.IsNullOrWhiteSpace(routeName); - var route = hasRoute ? routeName.ToLower().Trim() : null; + bool hasRoute = !string.IsNullOrWhiteSpace(slug); + var route = hasRoute ? slug.ToLower().Trim() : null; return await cache.GetOrCreateAsync( hasRoute ? BlogCachePartition.RssCategory.ToString() : BlogCachePartition.General.ToString(), route ?? "rss", async entry => { entry.SlidingExpiration = TimeSpan.FromHours(1); - var xml = await mediator.Send(new GetRssStringQuery(routeName)); + var xml = await mediator.Send(new GetRssStringQuery(slug)); if (string.IsNullOrWhiteSpace(xml)) { return (IActionResult)NotFound(); @@ -54,18 +54,18 @@ public async Task Rss([MaxLength(64)] string routeName = null) }); } - [HttpGet("atom/{routeName?}")] - public async Task Atom([MaxLength(64)] string routeName = null) + [HttpGet("atom/{slug?}")] + public async Task Atom([MaxLength(64)] string slug = null) { - bool hasRoute = !string.IsNullOrWhiteSpace(routeName); - var route = hasRoute ? routeName.ToLower().Trim() : null; + bool hasRoute = !string.IsNullOrWhiteSpace(slug); + var route = hasRoute ? slug.ToLower().Trim() : null; return await cache.GetOrCreateAsync( hasRoute ? BlogCachePartition.AtomCategory.ToString() : BlogCachePartition.General.ToString(), route ?? "atom", async entry => { entry.SlidingExpiration = TimeSpan.FromHours(1); - var xml = await mediator.Send(new GetAtomStringQuery(routeName)); + var xml = await mediator.Send(new GetAtomStringQuery(slug)); return Content(xml, "text/xml"); }); } diff --git a/src/Moonglade.Web/Controllers/TagsController.cs b/src/Moonglade.Web/Controllers/TagsController.cs index 99c095db1..b1a5a1437 100644 --- a/src/Moonglade.Web/Controllers/TagsController.cs +++ b/src/Moonglade.Web/Controllers/TagsController.cs @@ -1,4 +1,5 @@ using Moonglade.Core.TagFeature; +using Moonglade.Data.Entities; using System.ComponentModel.DataAnnotations; namespace Moonglade.Web.Controllers; @@ -9,7 +10,7 @@ namespace Moonglade.Web.Controllers; public class TagsController(IMediator mediator) : ControllerBase { [HttpGet("names")] - [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType>(StatusCodes.Status200OK)] public async Task Names() { var names = await mediator.Send(new GetTagNamesQuery()); @@ -17,7 +18,7 @@ public async Task Names() } [HttpGet("list")] - [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType>(StatusCodes.Status200OK)] public async Task List() { var list = await mediator.Send(new GetTagsQuery()); @@ -30,15 +31,14 @@ public async Task List() [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task Create([Required][FromBody] string name) { - if (string.IsNullOrWhiteSpace(name)) return BadRequest(); - if (!Tag.ValidateName(name)) return Conflict(); + if (!Helper.IsValidTagName(name)) return Conflict(); await mediator.Send(new CreateTagCommand(name.Trim())); return Ok(); } [HttpPut("{id:int}")] - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCacheType.PagingCount })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCacheType.PagingCount])] [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Put))] public async Task Update([Range(1, int.MaxValue)] int id, [Required][FromBody] string name) { @@ -49,7 +49,7 @@ public async Task Update([Range(1, int.MaxValue)] int id, [Requir } [HttpDelete("{id:int}")] - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCacheType.PagingCount })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCacheType.PagingCount])] [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Delete))] public async Task Delete([Range(0, int.MaxValue)] int id) { diff --git a/src/Moonglade.Web/Controllers/ThemeController.cs b/src/Moonglade.Web/Controllers/ThemeController.cs index 2ab98487a..4736a7c3b 100644 --- a/src/Moonglade.Web/Controllers/ThemeController.cs +++ b/src/Moonglade.Web/Controllers/ThemeController.cs @@ -1,5 +1,4 @@ -using NUglify; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Moonglade.Web.Controllers; @@ -10,7 +9,6 @@ public class ThemeController(IMediator mediator, ICacheAside cache, IBlogConfig [HttpGet("/theme.css")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType>(StatusCodes.Status409Conflict)] public async Task Css() { try @@ -33,10 +31,7 @@ public async Task Css() if (css == null) return NotFound(); - var uCss = Uglify.Css(css); - if (uCss.HasErrors) return Conflict(uCss.Errors); - - return Content(uCss.Code, "text/css; charset=utf-8"); + return Content(css, "text/css; charset=utf-8"); } catch (InvalidDataException e) { @@ -48,7 +43,7 @@ public async Task Css() [HttpPost] [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status200OK)] - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCachePartition.General, "theme" })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCachePartition.General, "theme"])] public async Task Create(CreateThemeRequest request) { var dic = new Dictionary @@ -59,7 +54,7 @@ public async Task Create(CreateThemeRequest request) }; var id = await mediator.Send(new CreateThemeCommand(request.Name, dic)); - if (id == 0) return Conflict("Theme with same name already exists"); + if (id == -1) return Conflict("Theme with same name already exists"); return Ok(id); } @@ -67,7 +62,7 @@ public async Task Create(CreateThemeRequest request) [Authorize] [HttpDelete("{id:int}")] [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Delete))] - [TypeFilter(typeof(ClearBlogCache), Arguments = new object[] { BlogCachePartition.General, "theme" })] + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCachePartition.General, "theme"])] public async Task Delete([Range(1, int.MaxValue)] int id) { var oc = await mediator.Send(new DeleteThemeCommand(id)); diff --git a/src/Moonglade.Web/IconGenerator.cs b/src/Moonglade.Web/IconGenerator.cs index f1412e595..2342f3b01 100644 --- a/src/Moonglade.Web/IconGenerator.cs +++ b/src/Moonglade.Web/IconGenerator.cs @@ -46,9 +46,9 @@ public static void GenerateIcons(string base64Data, string webRootPath, ILogger var dic = new Dictionary { - { "android-icon-", new[] { 144, 192 } }, - { "favicon-", new[] { 16, 32, 96 } }, - { "apple-icon-", new[] { 180 } } + { "android-icon-", [144, 192] }, + { "favicon-", [16, 32, 96] }, + { "apple-icon-", [180] } }; foreach (var (key, value) in dic) diff --git a/src/Moonglade.Web/MetaWeblogService.cs b/src/Moonglade.Web/MetaWeblogService.cs deleted file mode 100644 index d3bec1582..000000000 --- a/src/Moonglade.Web/MetaWeblogService.cs +++ /dev/null @@ -1,458 +0,0 @@ -using Moonglade.Core.CategoryFeature; -using Moonglade.Core.PageFeature; -using Moonglade.Core.PostFeature; -using Moonglade.Core.TagFeature; -using Moonglade.MetaWeblog; -using Post = Moonglade.MetaWeblog.Post; -using Tag = Moonglade.MetaWeblog.Tag; - -namespace Moonglade.Web; - -public class MetaWeblogService(IBlogConfig blogConfig, - ILogger logger, - IBlogImageStorage blogImageStorage, - IFileNameGenerator fileNameGenerator, - IMediator mediator) - : IMetaWeblogProvider -{ - public Task GetUserInfoAsync(string key, string username, string password) - { - EnsureUser(username, password); - - return TryExecute(() => - { - var user = new UserInfo - { - email = blogConfig.GeneralSettings.OwnerEmail, - firstname = blogConfig.GeneralSettings.OwnerName, - lastname = string.Empty, - nickname = string.Empty, - url = blogConfig.GeneralSettings.CanonicalPrefix, - userid = key - }; - - return Task.FromResult(user); - }); - } - - public Task GetUsersBlogsAsync(string key, string username, string password) - { - EnsureUser(username, password); - - return TryExecute(() => - { - var blog = new BlogInfo - { - blogid = blogConfig.GeneralSettings.SiteTitle, - blogName = blogConfig.GeneralSettings.SiteTitle, - url = "/" - }; - - return Task.FromResult(new[] { blog }); - }); - } - - public Task GetPostAsync(string postid, string username, string password) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - if (!Guid.TryParse(postid.Trim(), out var id)) - { - throw new ArgumentException("Invalid ID", nameof(postid)); - } - - var post = await mediator.Send(new GetPostByIdQuery(id)); - return ToMetaWeblogPost(post); - }); - } - - public async Task GetRecentPostsAsync(string blogid, string username, string password, int numberOfPosts) - { - EnsureUser(username, password); - await Task.CompletedTask; - - return TryExecute(() => - { - if (numberOfPosts < 0) throw new ArgumentOutOfRangeException(nameof(numberOfPosts)); - - // TODO: Get recent posts - return Array.Empty(); - }); - } - - public Task AddPostAsync(string blogid, string username, string password, Post post, bool publish) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - var cids = await GetCatIds(post.categories); - if (cids.Length == 0) - { - throw new ArgumentOutOfRangeException(nameof(post.categories)); - } - - var req = new PostEditModel - { - Title = post.title, - Slug = post.wp_slug ?? ToSlug(post.title), - EditorContent = post.description, - Tags = post.mt_keywords, - SelectedCatIds = cids, - LanguageCode = "en-us", - IsPublished = publish, - EnableComment = true, - FeedIncluded = true, - PublishDate = DateTime.UtcNow - }; - - var p = await mediator.Send(new CreatePostCommand(req)); - return p.Id.ToString(); - }); - } - - public Task DeletePostAsync(string key, string postid, string username, string password, bool publish) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - if (!Guid.TryParse(postid.Trim(), out var id)) - { - throw new ArgumentException("Invalid ID", nameof(postid)); - } - - await mediator.Send(new DeletePostCommand(id, publish)); - return true; - }); - } - - public Task EditPostAsync(string postid, string username, string password, Post post, bool publish) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - if (!Guid.TryParse(postid.Trim(), out var id)) - { - throw new ArgumentException("Invalid ID", nameof(postid)); - } - - var cids = await GetCatIds(post.categories); - if (cids.Length == 0) - { - throw new ArgumentOutOfRangeException(nameof(post.categories)); - } - - var req = new PostEditModel - { - Title = post.title, - Slug = post.wp_slug ?? ToSlug(post.title), - EditorContent = post.description, - Tags = post.mt_keywords, - SelectedCatIds = cids, - LanguageCode = "en-us", - IsPublished = publish, - EnableComment = true, - FeedIncluded = true, - PublishDate = DateTime.UtcNow - }; - - await mediator.Send(new UpdatePostCommand(id, req)); - return true; - }); - } - - public Task GetCategoriesAsync(string blogid, string username, string password) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - var cats = await mediator.Send(new GetCategoriesQuery()); - var catInfos = cats.Select(p => new CategoryInfo - { - title = p.DisplayName, - categoryid = p.Id.ToString(), - description = p.Note, - htmlUrl = $"/category/{p.RouteName}", - rssUrl = $"/rss/{p.RouteName}" - }).ToArray(); - - return catInfos; - }); - } - - public Task AddCategoryAsync(string key, string username, string password, NewCategory category) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - await mediator.Send(new CreateCategoryCommand - { - DisplayName = category.name.Trim(), - RouteName = category.slug.ToLower(), - Note = category.description.Trim() - }); - - return 996; - }); - } - - public Task GetTagsAsync(string blogid, string username, string password) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - var names = await mediator.Send(new GetTagNamesQuery()); - var tags = names.Select(p => new Tag - { - name = p - }).ToArray(); - - return tags; - }); - } - - public Task NewMediaObjectAsync(string blogid, string username, string password, MediaObject mediaObject) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - // TODO: Check extension names - - var bits = Convert.FromBase64String(mediaObject.bits); - - var pFilename = fileNameGenerator.GetFileName(mediaObject.name); - var filename = await blogImageStorage.InsertAsync(pFilename, bits); - - var imageUrl = $"{Helper.ResolveRootUrl(null, blogConfig.GeneralSettings.CanonicalPrefix, true)}image/{filename}"; - - var objectInfo = new MediaObjectInfo { url = imageUrl }; - return objectInfo; - }); - } - - public Task GetPageAsync(string blogid, string pageid, string username, string password) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - if (!Guid.TryParse(pageid, out var id)) - { - throw new ArgumentException("Invalid ID", nameof(pageid)); - } - - var page = await mediator.Send(new GetPageByIdQuery(id)); - return ToMetaWeblogPage(page); - }); - } - - public Task GetPagesAsync(string blogid, string username, string password, int numPages) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - if (numPages < 0) throw new ArgumentOutOfRangeException(nameof(numPages)); - - var pages = await mediator.Send(new GetPagesQuery(numPages)); - var mPages = pages.Select(ToMetaWeblogPage); - - return mPages.ToArray(); - }); - } - - public async Task GetAuthorsAsync(string blogid, string username, string password) - { - EnsureUser(username, password); - await Task.CompletedTask; - - return TryExecute(() => - { - return new[] - { - new Author - { - display_name = blogConfig.GeneralSettings.OwnerName - } - }; - }); - } - - public Task AddPageAsync(string blogid, string username, string password, Page page, bool publish) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - var pageRequest = new EditPageRequest - { - Title = page.title, - HideSidebar = true, - MetaDescription = string.Empty, - RawHtmlContent = page.description, - CssContent = string.Empty, - IsPublished = publish, - Slug = ToSlug(page.title) - }; - - var uid = await mediator.Send(new CreatePageCommand(pageRequest)); - return uid.ToString(); - }); - } - - public Task EditPageAsync(string blogid, string pageid, string username, string password, Page page, bool publish) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - if (!Guid.TryParse(pageid, out var id)) - { - throw new ArgumentException("Invalid ID", nameof(pageid)); - } - - var pageRequest = new EditPageRequest - { - Title = page.title, - HideSidebar = true, - MetaDescription = string.Empty, - RawHtmlContent = page.description, - CssContent = string.Empty, - IsPublished = publish, - Slug = ToSlug(page.title) - }; - - await mediator.Send(new UpdatePageCommand(id, pageRequest)); - return true; - }); - } - - public Task DeletePageAsync(string blogid, string username, string password, string pageid) - { - EnsureUser(username, password); - - return TryExecuteAsync(async () => - { - if (!Guid.TryParse(pageid, out var id)) - { - throw new ArgumentException("Invalid ID", nameof(pageid)); - } - - await mediator.Send(new DeletePageCommand(id)); - return true; - }); - } - - private void EnsureUser(string username, string password) - { - if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) - { - throw new ArgumentNullException(nameof(password)); - } - - var pwdHash = Helper.HashPassword(password.Trim()); - - if (string.Compare(username.Trim(), "moonglade", StringComparison.Ordinal) == 0 && - string.Compare(pwdHash, blogConfig.AdvancedSettings.MetaWeblogPasswordHash.Trim(), StringComparison.Ordinal) == 0) return; - - throw new MetaWeblogException("Authentication failed."); - } - - private string ToSlug(string title) - { - var engSlug = title.GenerateSlug(); - if (!string.IsNullOrWhiteSpace(engSlug)) return engSlug; - - // Chinese and other language title - var bytes = Encoding.Unicode.GetBytes(title); - var hexArray = bytes.Select(b => $"{b:x2}"); - var hexName = string.Join(string.Empty, hexArray); - - return hexName; - } - - private Post ToMetaWeblogPost(Core.PostFeature.Post post) - { - if (!post.IsPublished) return null; - var pubDate = post.PubDateUtc.GetValueOrDefault(); - var link = $"/post/{pubDate.Year}/{pubDate.Month}/{pubDate.Day}/{post.Slug.Trim().ToLower()}"; - - var mPost = new Post - { - postid = post.Id, - categories = post.Categories.Select(p => p.DisplayName).ToArray(), - dateCreated = post.CreateTimeUtc, - description = post.ContentAbstract, - link = link, - permalink = $"{Helper.ResolveRootUrl(null, blogConfig.GeneralSettings.CanonicalPrefix, true)}/{link}", - title = post.Title, - wp_slug = post.Slug, - mt_keywords = string.Join(',', post.Tags.Select(p => p.DisplayName)), - mt_excerpt = post.ContentAbstract, - userid = blogConfig.GeneralSettings.OwnerName - }; - - return mPost; - } - - private Page ToMetaWeblogPage(BlogPage blogPage) - { - var mPage = new Page - { - title = blogPage.Title, - description = blogPage.RawHtmlContent, - dateCreated = blogPage.CreateTimeUtc, - categories = Array.Empty(), - page_id = blogPage.Id.ToString(), - wp_author_id = blogConfig.GeneralSettings.OwnerName - }; - - return mPage; - } - - private async Task GetCatIds(string[] mPostCategories) - { - var allCats = await mediator.Send(new GetCategoriesQuery()); - var cids = (from postCategory in mPostCategories - select allCats.FirstOrDefault(category => category.DisplayName == postCategory) - into cat - where null != cat - select cat.Id).ToArray(); - - return cids; - } - - private T TryExecute(Func action) - { - try - { - return action(); - } - catch (Exception e) - { - logger.LogError(e, e.Message); - throw new MetaWeblogException(e.Message); - } - } - - private async Task TryExecuteAsync(Func> func) - { - try - { - return await func(); - } - catch (Exception e) - { - logger.LogError(e, e.Message); - throw new MetaWeblogException(e.Message); - } - } -} \ No newline at end of file diff --git a/src/Moonglade.Web/Middleware/RSDMiddleware.cs b/src/Moonglade.Web/Middleware/RSDMiddleware.cs deleted file mode 100644 index e03010711..000000000 --- a/src/Moonglade.Web/Middleware/RSDMiddleware.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Xml; - -namespace Moonglade.Web.Middleware; - -// Really Simple Discovery (RSD) is a protocol or method that makes it easier for client software to automatically discover the API endpoint needed to interact with a web service. It's primarily used in the context of blog software and content management systems (CMS). -// -// RSD allows client applications, like blog editors and content aggregators, to find the services needed to read, edit, or work with the content of a website without the user having to manually input the details of the API endpoints. For example, if you're using a desktop blogging application, RSD would enable that application to find the endpoint for the XML-RPC API of your blog so you can post directly from your desktop. - -public class RSDMiddleware(RequestDelegate next) -{ - public async Task Invoke(HttpContext httpContext, IBlogConfig blogConfig) - { - if (httpContext.Request.Path == "/rsd") - { - var siteRootUrl = Helper.ResolveRootUrl(httpContext, blogConfig.GeneralSettings.CanonicalPrefix, true); - var xml = await GetRSDData(siteRootUrl); - - httpContext.Response.ContentType = "text/xml"; - await httpContext.Response.WriteAsync(xml, httpContext.RequestAborted); - } - else - { - await next(httpContext); - } - } - - private static async Task GetRSDData(string siteRootUrl) - { - var sb = new StringBuilder(); - - var writerSettings = new XmlWriterSettings { Encoding = Encoding.UTF8, Async = true }; - await using (var writer = XmlWriter.Create(sb, writerSettings)) - { - await writer.WriteStartDocumentAsync(); - - // Rsd tag - writer.WriteStartElement("rsd"); - writer.WriteAttributeString("version", "1.0"); - - // Service - writer.WriteStartElement("service"); - writer.WriteElementString("engineName", $"Moonglade {Helper.AppVersion}"); - writer.WriteElementString("engineLink", "https://github.com/EdiWang/Moonglade"); - writer.WriteElementString("homePageLink", siteRootUrl); - - // APIs - writer.WriteStartElement("apis"); - - // MetaWeblog - writer.WriteStartElement("api"); - writer.WriteAttributeString("name", "MetaWeblog"); - writer.WriteAttributeString("preferred", "true"); - writer.WriteAttributeString("apiLink", $"{siteRootUrl}metaweblog"); - writer.WriteAttributeString("blogID", siteRootUrl); - await writer.WriteEndElementAsync(); - - // End APIs - await writer.WriteEndElementAsync(); - - // End Service - await writer.WriteEndElementAsync(); - - // End Rsd - await writer.WriteEndElementAsync(); - - await writer.WriteEndDocumentAsync(); - } - - var xml = sb.ToString(); - return xml; - } -} \ No newline at end of file diff --git a/src/Moonglade.Web/Middleware/SiteMapMiddleware.cs b/src/Moonglade.Web/Middleware/SiteMapMiddleware.cs index 851001927..ec9b054f9 100644 --- a/src/Moonglade.Web/Middleware/SiteMapMiddleware.cs +++ b/src/Moonglade.Web/Middleware/SiteMapMiddleware.cs @@ -1,6 +1,5 @@ using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; -using Moonglade.Data.Spec; +using Moonglade.Data.Specifications; using System.Globalization; using System.Xml; @@ -12,8 +11,8 @@ public async Task Invoke( HttpContext httpContext, IBlogConfig blogConfig, ICacheAside cache, - IRepository postRepo, - IRepository pageRepo) + MoongladeRepository postRepo, + MoongladeRepository pageRepo) { var xml = await cache.GetOrCreateAsync(BlogCachePartition.General.ToString(), "sitemap", async _ => { @@ -28,8 +27,8 @@ public async Task Invoke( private static async Task GetSiteMapData( string siteRootUrl, - IRepository postRepo, - IRepository pageRepo, + MoongladeRepository postRepo, + MoongladeRepository pageRepo, CancellationToken ct) { var sb = new StringBuilder(); @@ -41,34 +40,28 @@ private static async Task GetSiteMapData( writer.WriteStartElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9"); // Posts - var spec = new PostSitePageSpec(); - var posts = await postRepo - .SelectAsync(spec, p => new Tuple(p.Slug, p.PubDateUtc, p.LastModifiedUtc), ct); + var spec = new PostSiteMapSpec(); + var posts = await postRepo.ListAsync(spec, ct); - foreach (var (slug, pubDateUtc, lastModifyUtc) in posts.OrderByDescending(p => p.Item2)) + foreach (var item in posts.OrderByDescending(p => p.UpdateTimeUtc)) { - var pubDate = pubDateUtc.GetValueOrDefault(); + var pubDate = item.CreateTimeUtc; writer.WriteStartElement("url"); - writer.WriteElementString("loc", $"{siteRootUrl}/post/{pubDate.Year}/{pubDate.Month}/{pubDate.Day}/{slug.ToLower()}"); + writer.WriteElementString("loc", $"{siteRootUrl}/post/{pubDate.Year}/{pubDate.Month}/{pubDate.Day}/{item.Slug.ToLower()}"); writer.WriteElementString("lastmod", pubDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); - writer.WriteElementString("changefreq", GetChangeFreq(pubDateUtc.GetValueOrDefault(), lastModifyUtc)); + writer.WriteElementString("changefreq", GetChangeFreq(pubDate, item.UpdateTimeUtc)); await writer.WriteEndElementAsync(); } // Pages - var pages = await pageRepo.SelectAsync(page => new Tuple( - page.CreateTimeUtc, - page.UpdateTimeUtc, - page.Slug, - page.IsPublished), ct); - - foreach (var (createdTimeUtc, updateTimeUtc, slug, isPublished) in pages.Where(p => p.Item4)) + var pages = await pageRepo.ListAsync(new PageSitemapSpec(), ct); + foreach (var page in pages) { writer.WriteStartElement("url"); - writer.WriteElementString("loc", $"{siteRootUrl}/page/{slug.ToLower()}"); - writer.WriteElementString("lastmod", createdTimeUtc.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); - writer.WriteElementString("changefreq", GetChangeFreq(createdTimeUtc, updateTimeUtc)); + writer.WriteElementString("loc", $"{siteRootUrl}/page/{page.Slug.ToLower()}"); + writer.WriteElementString("lastmod", page.CreateTimeUtc.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + writer.WriteElementString("changefreq", GetChangeFreq(page.CreateTimeUtc, page.UpdateTimeUtc)); await writer.WriteEndElementAsync(); } diff --git a/src/Moonglade.Web/Middleware/StyleSheetMiddleware.cs b/src/Moonglade.Web/Middleware/StyleSheetMiddleware.cs index cd7d9da40..d85945699 100644 --- a/src/Moonglade.Web/Middleware/StyleSheetMiddleware.cs +++ b/src/Moonglade.Web/Middleware/StyleSheetMiddleware.cs @@ -1,5 +1,4 @@ -using NUglify; -using System.Web; +using System.Web; namespace Moonglade.Web.Middleware; @@ -70,16 +69,9 @@ private static async Task WriteStyleSheet(HttpContext context, string cssCode) return; } - var uglifiedCss = Uglify.Css(cssCode); - if (uglifiedCss.HasErrors) - { - context.Response.StatusCode = StatusCodes.Status409Conflict; - return; - } - context.Response.StatusCode = StatusCodes.Status200OK; context.Response.ContentType = "text/css; charset=utf-8"; - await context.Response.WriteAsync(uglifiedCss.Code, context.RequestAborted); + await context.Response.WriteAsync(cssCode, context.RequestAborted); } } diff --git a/src/Moonglade.Web/Middleware/WriteFoafCommand.cs b/src/Moonglade.Web/Middleware/WriteFoafCommand.cs index 454febcb7..e8db2a074 100644 --- a/src/Moonglade.Web/Middleware/WriteFoafCommand.cs +++ b/src/Moonglade.Web/Middleware/WriteFoafCommand.cs @@ -4,14 +4,14 @@ namespace Moonglade.Web.Middleware; -public class WriteFoafCommand(FoafDoc doc, string currentRequestUrl, IReadOnlyList links) +public class WriteFoafCommand(FoafDoc doc, string currentRequestUrl, List links) : IRequest { public FoafDoc Doc { get; set; } = doc; public string CurrentRequestUrl { get; set; } = currentRequestUrl; - public IReadOnlyList Links { get; set; } = links; + public List Links { get; set; } = links; public static string ContentType => "application/rdf+xml"; } @@ -50,7 +50,7 @@ public async Task Handle(WriteFoafCommand request, CancellationToken ct) Blog = request.Doc.BlogUrl, Email = request.Doc.Email, PhotoUrl = request.Doc.PhotoUrl, - Friends = new() + Friends = [] }; foreach (var friend in request.Links) diff --git a/src/Moonglade.Web/Moonglade.Web.csproj b/src/Moonglade.Web/Moonglade.Web.csproj index d0bf11fc8..d530fc6cd 100644 --- a/src/Moonglade.Web/Moonglade.Web.csproj +++ b/src/Moonglade.Web/Moonglade.Web.csproj @@ -38,7 +38,7 @@ - + @@ -50,7 +50,6 @@ - diff --git a/src/Moonglade.Web/PagedList/BasePagedList.cs b/src/Moonglade.Web/PagedList/BasePagedList.cs index 3a109f3a4..cca09d5c5 100644 --- a/src/Moonglade.Web/PagedList/BasePagedList.cs +++ b/src/Moonglade.Web/PagedList/BasePagedList.cs @@ -15,7 +15,7 @@ namespace Moonglade.Web.PagedList; /// public class BasePagedList : PagedListMetaData, IPagedList { - protected readonly List Subset = new(); + protected readonly List Subset = []; /// /// Parameterless constructor. diff --git a/src/Moonglade.Web/PagedList/HtmlHelper.cs b/src/Moonglade.Web/PagedList/HtmlHelper.cs index 3c2758443..abb4397fd 100644 --- a/src/Moonglade.Web/PagedList/HtmlHelper.cs +++ b/src/Moonglade.Web/PagedList/HtmlHelper.cs @@ -29,17 +29,7 @@ private static string TagBuilderToString(TagBuilder tagBuilder) return writer.ToString(); } - private TagBuilder WrapInListItem(string text) - { - var li = tagBuilderFactory - .Create("li"); - - SetInnerText(li, text); - - return li; - } - - private TagBuilder WrapInListItem(TagBuilder inner, PagedListRenderOptions options, params string[] classes) + private TagBuilder WrapInListItem(TagBuilder inner, params string[] classes) { var li = tagBuilderFactory.Create("li"); @@ -48,11 +38,6 @@ private TagBuilder WrapInListItem(TagBuilder inner, PagedListRenderOptions optio li.AddCssClass(@class); } - if (options?.FunctionToTransformEachPageLink != null) - { - return options.FunctionToTransformEachPageLink(li, inner); - } - AppendHtml(li, TagBuilderToString(inner)); return li; @@ -73,12 +58,12 @@ private TagBuilder First(IPagedList list, Func generatePageUrl, Pag if (list.IsFirstPage) { - return WrapInListItem(first, options, "paged-list-skip-to-first", "disabled"); + return WrapInListItem(first, "paged-list-skip-to-first", "disabled"); } first.Attributes.Add("href", generatePageUrl(targetPageNumber)); - return WrapInListItem(first, options, "paged-list-skip-to-first"); + return WrapInListItem(first, "paged-list-skip-to-first"); } private TagBuilder Previous(IPagedList list, Func generatePageUrl, PagedListRenderOptions options) @@ -98,12 +83,12 @@ private TagBuilder Previous(IPagedList list, Func generatePageUrl, if (!list.HasPreviousPage) { - return WrapInListItem(previous, options, options.PreviousElementClass, "disabled"); + return WrapInListItem(previous, options.PreviousElementClass, "disabled"); } previous.Attributes.Add("href", generatePageUrl(targetPageNumber)); - return WrapInListItem(previous, options, options.PreviousElementClass); + return WrapInListItem(previous, options.PreviousElementClass); } private TagBuilder Page(int i, IPagedList list, Func generatePageUrl, PagedListRenderOptions options) @@ -126,12 +111,12 @@ private TagBuilder Page(int i, IPagedList list, Func generatePageUr if (i == list.PageNumber) { - return WrapInListItem(page, options, options.ActiveLiElementClass); + return WrapInListItem(page, "active"); } page.Attributes.Add("href", generatePageUrl(targetPageNumber)); - return WrapInListItem(page, options); + return WrapInListItem(page); } private TagBuilder Next(IPagedList list, Func generatePageUrl, PagedListRenderOptions options) @@ -151,12 +136,12 @@ private TagBuilder Next(IPagedList list, Func generatePageUrl, Page if (!list.HasNextPage) { - return WrapInListItem(next, options, options.NextElementClass, "disabled"); + return WrapInListItem(next, options.NextElementClass, "disabled"); } next.Attributes.Add("href", generatePageUrl(targetPageNumber)); - return WrapInListItem(next, options, options.NextElementClass); + return WrapInListItem(next, options.NextElementClass); } private TagBuilder Last(IPagedList list, Func generatePageUrl, PagedListRenderOptions options) @@ -174,12 +159,12 @@ private TagBuilder Last(IPagedList list, Func generatePageUrl, Page if (list.IsLastPage) { - return WrapInListItem(last, options, "paged-list-skip-to-last", "disabled"); + return WrapInListItem(last, "paged-list-skip-to-last", "disabled"); } last.Attributes.Add("href", generatePageUrl(targetPageNumber)); - return WrapInListItem(last, options, "paged-list-skip-to-last"); + return WrapInListItem(last, "paged-list-skip-to-last"); } private TagBuilder PageCountAndLocationText(IPagedList list, PagedListRenderOptions options) @@ -189,71 +174,7 @@ private TagBuilder PageCountAndLocationText(IPagedList list, PagedListRenderOpti SetInnerText(text, string.Format(options.PageCountAndCurrentLocationFormat, list.PageNumber, list.PageCount)); - return WrapInListItem(text, options, "PagedList-pageCountAndLocation", "disabled"); - } - - private TagBuilder ItemSliceAndTotalText(IPagedList list, PagedListRenderOptions options) - { - var text = tagBuilderFactory - .Create("a"); - - SetInnerText(text, string.Format(options.ItemSliceAndTotalFormat, list.FirstItemOnPage, list.LastItemOnPage, list.TotalItemCount)); - - return WrapInListItem(text, options, "PagedList-pageCountAndLocation", "disabled"); - } - - private TagBuilder PreviousEllipsis(IPagedList list, Func generatePageUrl, PagedListRenderOptions options, int firstPageToDisplay) - { - var previous = tagBuilderFactory - .Create("a"); - - AppendHtml(previous, options.EllipsesFormat); - - previous.Attributes.Add("rel", "prev"); - previous.AddCssClass("paged-list-skip-to-previous"); - - foreach (var c in options.PageClasses ?? Enumerable.Empty()) - { - previous.AddCssClass(c); - } - - if (!list.HasPreviousPage) - { - return WrapInListItem(previous, options, options.EllipsesElementClass, "disabled"); - } - - var targetPageNumber = firstPageToDisplay - 1; - - previous.Attributes.Add("href", generatePageUrl(targetPageNumber)); - - return WrapInListItem(previous, options, options.EllipsesElementClass); - } - - private TagBuilder NextEllipsis(IPagedList list, Func generatePageUrl, PagedListRenderOptions options, int lastPageToDisplay) - { - var next = tagBuilderFactory - .Create("a"); - - AppendHtml(next, options.EllipsesFormat); - - next.Attributes.Add("rel", "next"); - next.AddCssClass("paged-list-skip-to-next"); - - foreach (var c in options.PageClasses ?? Enumerable.Empty()) - { - next.AddCssClass(c); - } - - if (!list.HasNextPage) - { - return WrapInListItem(next, options, options.EllipsesElementClass, "disabled"); - } - - var targetPageNumber = lastPageToDisplay + 1; - - next.Attributes.Add("href", generatePageUrl(targetPageNumber)); - - return WrapInListItem(next, options, options.EllipsesElementClass); + return WrapInListItem(text, "PagedList-pageCountAndLocation", "disabled"); } #endregion Private methods @@ -313,40 +234,8 @@ public string PagedListPager(IPagedList pagedList, Func generatePag listItemLinks.Add(PageCountAndLocationText(list, options)); } - //text - if (options.DisplayItemSliceAndTotal && options.ItemSliceAndTotalPosition == ItemSliceAndTotalPosition.Start) - { - listItemLinks.Add(ItemSliceAndTotalText(list, options)); - } - //page - if (options.DisplayLinkToIndividualPages) - { - //if there are previous page numbers not displayed, show an ellipsis - if (options.DisplayEllipsesWhenNotShowingAllPageNumbers && firstPageToDisplay > 1) - { - listItemLinks.Add(PreviousEllipsis(list, generatePageUrl, options, firstPageToDisplay)); - } - - foreach (var i in Enumerable.Range(firstPageToDisplay, pageNumbersToDisplay)) - { - //show delimiter between page numbers - if (i > firstPageToDisplay && !string.IsNullOrWhiteSpace(options.DelimiterBetweenPageNumbers)) - { - listItemLinks.Add(WrapInListItem(options.DelimiterBetweenPageNumbers)); - } - - //show page number link - listItemLinks.Add(Page(i, list, generatePageUrl, options)); - } - - //if there are subsequent page numbers not displayed, show an ellipsis - if (options.DisplayEllipsesWhenNotShowingAllPageNumbers && - (firstPageToDisplay + pageNumbersToDisplay - 1) < list.PageCount) - { - listItemLinks.Add(NextEllipsis(list, generatePageUrl, options, lastPageToDisplay)); - } - } + listItemLinks.AddRange(Enumerable.Range(firstPageToDisplay, pageNumbersToDisplay).Select(i => Page(i, list, generatePageUrl, options))); //next if (!list.IsLastPage) @@ -360,21 +249,12 @@ public string PagedListPager(IPagedList pagedList, Func generatePag listItemLinks.Add(Last(list, generatePageUrl, options)); } - //text - if (options.DisplayItemSliceAndTotal && options.ItemSliceAndTotalPosition == ItemSliceAndTotalPosition.End) - { - listItemLinks.Add(ItemSliceAndTotalText(list, options)); - } - if (listItemLinks.Any()) { //append classes to all list item links foreach (var li in listItemLinks) { - foreach (var c in options.LiElementClasses ?? Enumerable.Empty()) - { - li.AddCssClass(c); - } + li.AddCssClass("page-item"); } } @@ -397,10 +277,7 @@ public string PagedListPager(IPagedList pagedList, Func generatePag var outerDiv = tagBuilderFactory .Create("div"); - foreach (var c in options.ContainerDivClasses ?? Enumerable.Empty()) - { - outerDiv.AddCssClass(c); - } + outerDiv.AddCssClass("pagination-container"); AppendHtml(outerDiv, TagBuilderToString(ul)); diff --git a/src/Moonglade.Web/PagedList/HtmlHelperExtension.cs b/src/Moonglade.Web/PagedList/HtmlHelperExtension.cs index d1d7ef284..8fe96b64c 100644 --- a/src/Moonglade.Web/PagedList/HtmlHelperExtension.cs +++ b/src/Moonglade.Web/PagedList/HtmlHelperExtension.cs @@ -11,11 +11,11 @@ public static HtmlString PagedListPager(this IHtmlHelper html, Func generatePageUrl, PagedListRenderOptions options) { - var htmlHelper = new HtmlHelper(new TagBuilderFactory()); + var htmlHelper = new HtmlHelper(new()); var htmlString = htmlHelper.PagedListPager(list, generatePageUrl, options); htmlString = HttpUtility.HtmlDecode(htmlString); - return new HtmlString(htmlString); + return new(htmlString); } } \ No newline at end of file diff --git a/src/Moonglade.Web/PagedList/ItemSliceAndTotalPosition.cs b/src/Moonglade.Web/PagedList/ItemSliceAndTotalPosition.cs deleted file mode 100644 index 8f8338fbe..000000000 --- a/src/Moonglade.Web/PagedList/ItemSliceAndTotalPosition.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Moonglade.Web.PagedList; - -/// -/// A two-state enum that controls the position of ItemSliceAndTotal text within PagedList items. -/// -public enum ItemSliceAndTotalPosition -{ - /// - /// Shows ItemSliceAndTotal info at the beginning of the PagedList items. - /// - Start, - - /// - /// Shows ItemSliceAndTotal info at the end of the PagedList items. - /// - End -} \ No newline at end of file diff --git a/src/Moonglade.Web/PagedList/PagedListMetaData.cs b/src/Moonglade.Web/PagedList/PagedListMetaData.cs index 5d942494e..6fd94f3da 100644 --- a/src/Moonglade.Web/PagedList/PagedListMetaData.cs +++ b/src/Moonglade.Web/PagedList/PagedListMetaData.cs @@ -1,5 +1,4 @@ - -namespace Moonglade.Web.PagedList; +namespace Moonglade.Web.PagedList; /// /// Non-enumerable version of the PagedList class. @@ -31,8 +30,6 @@ public PagedListMetaData(IPagedList pagedList) LastItemOnPage = pagedList.LastItemOnPage; } - #region IPagedList Members - /// /// Total number of subsets within the superset. /// @@ -122,6 +119,4 @@ public PagedListMetaData(IPagedList pagedList) /// is greater than PageCount. /// public int LastItemOnPage { get; protected set; } - - #endregion } \ No newline at end of file diff --git a/src/Moonglade.Web/PagedList/PagedListRenderOptions.cs b/src/Moonglade.Web/PagedList/PagedListRenderOptions.cs index 1a2ede7a4..31db19c25 100644 --- a/src/Moonglade.Web/PagedList/PagedListRenderOptions.cs +++ b/src/Moonglade.Web/PagedList/PagedListRenderOptions.cs @@ -1,7 +1,4 @@ -using Microsoft.AspNetCore.Mvc.Rendering; -using System.Text.Encodings.Web; - -namespace Moonglade.Web.PagedList; +namespace Moonglade.Web.PagedList; public class PagedListRenderOptions { @@ -10,12 +7,8 @@ public class PagedListRenderOptions /// public PagedListRenderOptions() { - HtmlEncoder = HtmlEncoder.Default; - DisplayLinkToIndividualPages = true; DisplayPageCountAndCurrentLocation = false; MaximumPageNumbersToDisplay = 10; - DisplayEllipsesWhenNotShowingAllPageNumbers = true; - EllipsesFormat = "…"; LinkToFirstPageFormat = "<<"; LinkToPreviousPageFormat = "<"; LinkToIndividualPageFormat = "{0}"; @@ -23,43 +16,18 @@ public PagedListRenderOptions() LinkToLastPageFormat = ">>"; PageCountAndCurrentLocationFormat = "Page {0} of {1}."; ItemSliceAndTotalFormat = "Showing items {0} through {1} of {2}."; - ItemSliceAndTotalPosition = ItemSliceAndTotalPosition.Start; FunctionToDisplayEachPageNumber = null; - ContainerDivClasses = new[] { "pagination-container" }; - UlElementClasses = new[] { "pagination" }; - LiElementClasses = new[] { "page-item" }; - PageClasses = new[] { "page-link" }; - ActiveLiElementClass = "active"; - EllipsesElementClass = "paged-list-ellipses"; + UlElementClasses = ["pagination"]; + PageClasses = ["page-link"]; PreviousElementClass = "PagedList-skip-to-previous"; NextElementClass = "paged-list-skip-to-next"; } - /// - /// Gets or sets the HtmlEncoder to use encoding HTML render. - /// - public HtmlEncoder HtmlEncoder { get; set; } - - /// - /// CSS Classes to append to the <div> element that wraps the paging control. - /// - public IEnumerable ContainerDivClasses { get; set; } - /// /// CSSClasses to append to the <ul> element in the paging control. /// public IEnumerable UlElementClasses { get; set; } - /// - /// CSS Classes to append to every <li> element in the paging control. - /// - public IEnumerable LiElementClasses { get; set; } - - /// - /// CSS Classes to appent to active <li> element in the paging control. - /// - public string ActiveLiElementClass { get; set; } - /// /// CSS Classes to append to every <a> or <span> element that represent each page in the paging control. /// @@ -75,16 +43,6 @@ public PagedListRenderOptions() /// public string NextElementClass { get; set; } - /// - /// CSS Classes to append to Ellipses element in the paging control. - /// - public string EllipsesElementClass { get; set; } - - /// - /// When true, includes hyperlinks for each page in the list. - /// - public bool DisplayLinkToIndividualPages { get; set; } - /// /// When true, shows the current page number and the total number of pages in the list. /// @@ -93,37 +51,11 @@ public PagedListRenderOptions() /// public bool DisplayPageCountAndCurrentLocation { get; set; } - /// - /// When true, shows the one-based index of the first and last items on the page, and the total number of items in the list. - /// - /// - /// "Showing items 75 through 100 of 183." - /// - public bool DisplayItemSliceAndTotal { get; set; } - /// /// The maximum number of page numbers to display. Null displays all page numbers. /// public int? MaximumPageNumbersToDisplay { get; set; } - /// - /// If true, adds an ellipsis where not all page numbers are being displayed. - /// - /// - /// "1 2 3 4 5 ...", - /// "... 6 7 8 9 10 ...", - /// "... 11 12 13 14 15" - /// - public bool DisplayEllipsesWhenNotShowingAllPageNumbers { get; set; } - - /// - /// The pre-formatted text to display when not all page numbers are displayed at once. - /// - /// - /// "..." - /// - public string EllipsesFormat { get; set; } - /// /// The pre-formatted text to display inside the hyperlink to the first page. The one-based index of the page (always 1 in this case) is passed into the formatting function - use {0} to reference it. /// @@ -180,23 +112,8 @@ public PagedListRenderOptions() /// public string ItemSliceAndTotalFormat { get; set; } - /// - /// If set to , render an info at the beginning of the list. If set to , render the at the beginning of the list. - /// - public ItemSliceAndTotalPosition ItemSliceAndTotalPosition { get; set; } - /// /// A function that will render each page number when specified (and DisplayLinkToIndividualPages is true). If no function is specified, the LinkToIndividualPageFormat value will be used instead. /// public Func FunctionToDisplayEachPageNumber { get; set; } - - /// - /// Text that will appear between each page number. If null or whitespace is specified, no delimiter will be shown. - /// - public string DelimiterBetweenPageNumbers { get; set; } - - /// - /// An extension point which allows you to fully customize the anchor tags used for clickable pages, as well as navigation features such as Next, Last, etc. - /// - public Func FunctionToTransformEachPageLink { get; set; } } \ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Admin/About.cshtml b/src/Moonglade.Web/Pages/Admin/About.cshtml index 6b98595fb..b35724ee1 100644 --- a/src/Moonglade.Web/Pages/Admin/About.cshtml +++ b/src/Moonglade.Web/Pages/Admin/About.cshtml @@ -91,10 +91,10 @@ @SharedLocalizer["Docker Container"] - @(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true") + @Helper.IsRunningInDocker() - @if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME"))) + @if (Helper.IsRunningOnAzureAppService()) { @SharedLocalizer["Azure App Service"] @@ -106,6 +106,16 @@ } + + @if (Request.Headers.ContainsKey("X-Azure-FDID")) + { + + @SharedLocalizer["Azure Front Door"] + + X-Azure-FDID: @Request.Headers["X-Azure-FDID"] + + + } diff --git a/src/Moonglade.Web/Pages/Admin/Category.cshtml b/src/Moonglade.Web/Pages/Admin/Category.cshtml index 7f89a4838..13c4dc154 100644 --- a/src/Moonglade.Web/Pages/Admin/Category.cshtml +++ b/src/Moonglade.Web/Pages/Admin/Category.cshtml @@ -21,7 +21,7 @@ async (resp) => { var data = await resp.json(); catId = data.id; - document.querySelector('#EditCategoryRequest_RouteName').value = data.routeName; + document.querySelector('#EditCategoryRequest_Slug').value = data.slug; document.querySelector('#EditCategoryRequest_DisplayName').value = data.displayName; document.querySelector('#EditCategoryRequest_Note').value = data.note; editCanvas.show(); @@ -61,7 +61,7 @@ callApi(apiAddress, verb, { - routeName: value["EditCategoryRequest.RouteName"], + slug: value["EditCategoryRequest.Slug"], displayName: value["EditCategoryRequest.DisplayName"], note: value["EditCategoryRequest.Note"] }, @@ -100,7 +100,7 @@
- + @cat.DisplayName
@@ -109,7 +109,7 @@ @cat.Note

- @cat.RouteName + @cat.Slug
- - + + @SharedLocalizer["lower case English letters (a-z) and numbers (0-9) with/out hyphen (-) in middle."] diff --git a/src/Moonglade.Web/Pages/Admin/Category.cshtml.cs b/src/Moonglade.Web/Pages/Admin/Category.cshtml.cs index acbbabf7b..b5161b504 100644 --- a/src/Moonglade.Web/Pages/Admin/Category.cshtml.cs +++ b/src/Moonglade.Web/Pages/Admin/Category.cshtml.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Moonglade.Core.CategoryFeature; +using Moonglade.Data.Entities; namespace Moonglade.Web.Pages.Admin; @@ -7,7 +8,7 @@ public class CategoryModel(IMediator mediator) : PageModel { public CreateCategoryCommand EditCategoryRequest { get; set; } = new(); - public IReadOnlyList Categories { get; set; } + public List Categories { get; set; } public async Task OnGet() => Categories = await mediator.Send(new GetCategoriesQuery()); } \ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Admin/Comments.cshtml b/src/Moonglade.Web/Pages/Admin/Comments.cshtml index 590f6ddb8..83d850bee 100644 --- a/src/Moonglade.Web/Pages/Admin/Comments.cshtml +++ b/src/Moonglade.Web/Pages/Admin/Comments.cshtml @@ -7,7 +7,7 @@ } @Html.AntiForgeryToken() -@section scripts{ +@section scripts { } -@section head{ +@section head { } -@section admintoolbar{ +@section admintoolbar { \ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Admin/Draft.cshtml b/src/Moonglade.Web/Pages/Admin/Draft.cshtml index e91e72df8..4aaaa3fe4 100644 --- a/src/Moonglade.Web/Pages/Admin/Draft.cshtml +++ b/src/Moonglade.Web/Pages/Admin/Draft.cshtml @@ -1,6 +1,6 @@ @page "/admin/post/draft" @using Moonglade.Core.PostFeature -@using Moonglade.Data.Spec +@using Moonglade.Data.Specifications @inject IMediator Mediator @{ ViewBag.Title = "Drafts"; diff --git a/src/Moonglade.Web/Pages/Admin/EditPage.cshtml.cs b/src/Moonglade.Web/Pages/Admin/EditPage.cshtml.cs index 80b57dcec..70d428d6b 100644 --- a/src/Moonglade.Web/Pages/Admin/EditPage.cshtml.cs +++ b/src/Moonglade.Web/Pages/Admin/EditPage.cshtml.cs @@ -31,7 +31,7 @@ public async Task OnGetAsync(Guid? id) Slug = page.Slug, MetaDescription = page.MetaDescription, CssContent = css?.CssContent, - RawHtmlContent = page.RawHtmlContent, + RawHtmlContent = page.HtmlContent, HideSidebar = page.HideSidebar, IsPublished = page.IsPublished }; diff --git a/src/Moonglade.Web/Pages/Admin/EditPost.cshtml.cs b/src/Moonglade.Web/Pages/Admin/EditPost.cshtml.cs index 4af01b6ca..c3b49c3ac 100644 --- a/src/Moonglade.Web/Pages/Admin/EditPost.cshtml.cs +++ b/src/Moonglade.Web/Pages/Admin/EditPost.cshtml.cs @@ -19,12 +19,13 @@ public class EditPostModel(IMediator mediator, ITimeZoneResolver timeZoneResolve public async Task OnGetAsync(Guid? id) { + var cats = await mediator.Send(new GetCategoriesQuery()); + if (id is null) { - var cats1 = await mediator.Send(new GetCategoriesQuery()); - if (cats1.Count > 0) + if (cats.Count > 0) { - var cbCatList = cats1.Select(p => + var cbCatList = cats.Select(p => new CategoryCheckBox { Id = p.Id, @@ -47,7 +48,7 @@ public async Task OnGetAsync(Guid? id) { PostId = post.Id, IsPublished = post.IsPublished, - EditorContent = post.RawPostContent, + EditorContent = post.PostContent, Author = post.Author, Slug = post.Slug, Title = post.Title, @@ -55,7 +56,7 @@ public async Task OnGetAsync(Guid? id) FeedIncluded = post.IsFeedIncluded, LanguageCode = post.ContentLanguageCode, Abstract = post.ContentAbstract.Replace("\u00A0\u2026", string.Empty), - Featured = post.Featured, + Featured = post.IsFeatured, OriginLink = post.OriginLink, HeroImageUrl = post.HeroImageUrl, IsOutdated = post.IsOutdated @@ -73,15 +74,14 @@ public async Task OnGetAsync(Guid? id) tagStr = tagStr.TrimEnd(','); ViewModel.Tags = tagStr; - var cats2 = await mediator.Send(new GetCategoriesQuery()); - if (cats2.Count > 0) + if (cats.Count > 0) { - var cbCatList = cats2.Select(p => + var cbCatList = cats.Select(p => new CategoryCheckBox { Id = p.Id, DisplayText = p.DisplayName, - IsChecked = post.Categories.Any(q => q.Id == p.Id) + IsChecked = post.PostCategory.Any(q => q.CategoryId == p.Id) }); CategoryList = cbCatList.ToList(); } diff --git a/src/Moonglade.Web/Pages/Admin/FriendLink.cshtml.cs b/src/Moonglade.Web/Pages/Admin/FriendLink.cshtml.cs index 160b72115..4f71a284e 100644 --- a/src/Moonglade.Web/Pages/Admin/FriendLink.cshtml.cs +++ b/src/Moonglade.Web/Pages/Admin/FriendLink.cshtml.cs @@ -6,9 +6,9 @@ namespace Moonglade.Web.Pages.Admin; public class FriendLinkModel(IMediator mediator) : PageModel { - public UpdateLinkCommand EditLinkRequest { get; set; } = new(); + public EditLinkRequest EditLinkRequest { get; set; } = new(); - public IReadOnlyList Links { get; set; } + public List Links { get; set; } public async Task OnGet() => Links = await mediator.Send(new GetAllLinksQuery()); } \ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Admin/LocalAccount.cshtml b/src/Moonglade.Web/Pages/Admin/LocalAccount.cshtml index 4f4c795f7..f5d4ae597 100644 --- a/src/Moonglade.Web/Pages/Admin/LocalAccount.cshtml +++ b/src/Moonglade.Web/Pages/Admin/LocalAccount.cshtml @@ -1,59 +1,20 @@ @page "/admin/account" -@model Moonglade.Web.Pages.Admin.LocalAccountModel +@using System.Globalization +@inject IMediator Mediator @{ - ViewBag.Title = "Accounts"; - var currentUidClaim = User.Claims.FirstOrDefault(c => c.Type == "uid"); + ViewBag.Title = "Account"; + var loginHistoryList = await Mediator.Send(new GetLoginHistoryQuery()); } -@section scripts{ +@section scripts { } - -@section admintoolbar{ +@section admintoolbar { } +
+ Local account is not secure. It's recommended to use Microsoft Entra ID for authentication. Click here to configure. +
- @if (null != Model.Accounts && Model.Accounts.Any()) - { - +

+ Last 10 sign in activities +

+ +@if (null != loginHistoryList && loginHistoryList.Any()) +{ +
+
- - - - - + + + - @foreach (var item in Model.Accounts.OrderBy(p => p.Username)) + @foreach (var item in loginHistoryList.OrderByDescending(p => p.LoginTimeUtc)) { - - - - - - + + + + }
@SharedLocalizer["Username"]@SharedLocalizer["Last Login IP"]@SharedLocalizer["Last Login Time (UTC)"]@SharedLocalizer["Create Time (UTC)"]@SharedLocalizer["Action"]@SharedLocalizer["Login IP"]@SharedLocalizer["Login Time (UTC)"]@SharedLocalizer["User Agent"]
@item.Username@(item.LastLoginIp ?? "N/A")@(null != item.LastLoginTimeUtc ? item.LastLoginTimeUtc.ToString() : "N/A")@item.CreateTimeUtc - - - - - @if (null != currentUidClaim && currentUidClaim.Value != item.Id.ToString()) - { - - } -
@item.LoginIp@item.LoginTimeUtc.ToString(CultureInfo.CurrentCulture)@item.LoginUserAgent
- } - -

Check out Microsoft Entra ID and empower your blog to achieve more!

- - - +}
- @if (Model.Post.Featured) + @if (Model.Post.IsFeatured) { } @@ -62,7 +62,7 @@ }
- +
@if (BlogConfig.ContentSettings.ShowPostFooter) { diff --git a/src/Moonglade.Web/Pages/PostPreview.cshtml.cs b/src/Moonglade.Web/Pages/PostPreview.cshtml.cs index c2ca1201a..646994499 100644 --- a/src/Moonglade.Web/Pages/PostPreview.cshtml.cs +++ b/src/Moonglade.Web/Pages/PostPreview.cshtml.cs @@ -1,12 +1,13 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Moonglade.Core.PostFeature; +using Moonglade.Data.Entities; namespace Moonglade.Web.Pages; [Authorize] public class PostPreviewModel(IMediator mediator) : PageModel { - public Post Post { get; set; } + public PostEntity Post { get; set; } public async Task OnGetAsync(Guid postId) { diff --git a/src/Moonglade.Web/Pages/Search.cshtml.cs b/src/Moonglade.Web/Pages/Search.cshtml.cs index f75c1fc35..1700f8c7b 100644 --- a/src/Moonglade.Web/Pages/Search.cshtml.cs +++ b/src/Moonglade.Web/Pages/Search.cshtml.cs @@ -5,7 +5,7 @@ namespace Moonglade.Web.Pages; public class SearchModel(IMediator mediator) : PageModel { - public IReadOnlyList Posts { get; set; } + public List Posts { get; set; } public async Task OnGetAsync(string term) { diff --git a/src/Moonglade.Web/Pages/Settings/Advanced.cshtml b/src/Moonglade.Web/Pages/Settings/Advanced.cshtml index f080e64e8..e9bbee8e1 100644 --- a/src/Moonglade.Web/Pages/Settings/Advanced.cshtml +++ b/src/Moonglade.Web/Pages/Settings/Advanced.cshtml @@ -28,17 +28,6 @@ }, 1000); } - $('.btn-restart').click(function () { - callApi(`/api/settings/shutdown`, 'POST', {}, () => { }); - $('.btn-restart').text('Wait...'); - $('.btn-restart').addClass('disabled'); - $('.btn-restart').attr('disabled', 'disabled'); - startTimer(30, $('.btn-restart')); - setTimeout(function () { - location.href = '/admin/settings'; - }, 30000); - }); - $('.btn-reset').click(function () { callApi(`/api/settings/reset`, 'POST', {}, () => { }); $('.btn-reset').text('Wait...'); @@ -92,13 +81,6 @@ }); }); - window.generateMetaWeblogPassword = function () { - callApi('/api/settings/password/generate', 'GET', {}, async (resp) => { - var data = await resp.json(); - $('#settings_MetaWeblogPassword').val(data.password); - }); - } - const form = document.querySelector('#form-settings'); form.addEventListener('submit', settings.handleSettingsSubmit); @@ -206,46 +188,6 @@
- -

- MetaWeblog -

- -
-
- -
-
- -
- @SharedLocalizer["* Requires restarting application"] -
-
-
-
- - -
-
-
- -
-
- -
-
- @SharedLocalizer["Password"] -
- @SharedLocalizer["Leave blank if you don't need to change password."] - @SharedLocalizer["* Username: moonglade"] -
-
- -
@@ -291,23 +233,6 @@
-
-
- -
-
- @SharedLocalizer["Restart Website"] -
- @SharedLocalizer["Try to shutdown and restart the website, this will terminate all current requests."] -
-
- -
-
diff --git a/src/Moonglade.Web/Pages/Settings/General.cshtml b/src/Moonglade.Web/Pages/Settings/General.cshtml index 3545088ca..110417dd6 100644 --- a/src/Moonglade.Web/Pages/Settings/General.cshtml +++ b/src/Moonglade.Web/Pages/Settings/General.cshtml @@ -64,7 +64,6 @@ }, async (resp) => { var id = await resp.json(); - console.info(id); themeModal.hide(); $("#ViewModel_SelectedThemeId").append(new Option(document.querySelector('#Name').value, id)); diff --git a/src/Moonglade.Web/Pages/Settings/Subscription.cshtml b/src/Moonglade.Web/Pages/Settings/Subscription.cshtml index 2faaa3d16..c969a48da 100644 --- a/src/Moonglade.Web/Pages/Settings/Subscription.cshtml +++ b/src/Moonglade.Web/Pages/Settings/Subscription.cshtml @@ -24,11 +24,11 @@
- -
@SharedLocalizer["The number of entries to include in RSS feed."]
+ +
@SharedLocalizer["The number of entries to include in RSS/ATOM feed."]
- +
diff --git a/src/Moonglade.Web/Pages/Shared/_AsideAdmin.cshtml b/src/Moonglade.Web/Pages/Shared/_AsideAdmin.cshtml index 4964aa736..7fce334c5 100644 --- a/src/Moonglade.Web/Pages/Shared/_AsideAdmin.cshtml +++ b/src/Moonglade.Web/Pages/Shared/_AsideAdmin.cshtml @@ -1,5 +1,4 @@ -@inject IOptions AuthOptions -@{ +@{ var currentPage = ViewContext.RouteData.Values["Page"]?.ToString(); } @@ -49,14 +48,6 @@ @SharedLocalizer["Pingbacks"] - @if (AuthOptions.Value.Provider == AuthenticationProvider.Local) - { - - - @SharedLocalizer["Accounts"] - - } \ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Shared/_Layout.cshtml b/src/Moonglade.Web/Pages/Shared/_Layout.cshtml index 252e71ace..fc7b0a346 100644 --- a/src/Moonglade.Web/Pages/Shared/_Layout.cshtml +++ b/src/Moonglade.Web/Pages/Shared/_Layout.cshtml @@ -1,6 +1,7 @@ @using Moonglade.Utils @using Microsoft.AspNetCore.Localization @using System.Globalization +@inject IConfiguration Configuration @{ if (string.IsNullOrEmpty(BlogConfig.GeneralSettings.AvatarUrl)) @@ -72,10 +73,6 @@ } - @if (BlogConfig.AdvancedSettings.EnableMetaWeblog) - { - - } @if (BlogConfig.AdvancedSettings.EnableFoaf) { @@ -96,7 +93,7 @@ - @if (!(bool)Context.Items["DNT"]!) + @if (bool.Parse(Configuration["RenderApplicationInsightsJs"]!) && !(bool)Context.Items["DNT"]!) { } diff --git a/src/Moonglade.Web/Pages/Shared/_LayoutAdmin.cshtml b/src/Moonglade.Web/Pages/Shared/_LayoutAdmin.cshtml index c82acbe38..c43f4f1f2 100644 --- a/src/Moonglade.Web/Pages/Shared/_LayoutAdmin.cshtml +++ b/src/Moonglade.Web/Pages/Shared/_LayoutAdmin.cshtml @@ -1,4 +1,5 @@ -@{ +@inject IOptions AuthOptions +@{ if (string.IsNullOrEmpty(BlogConfig.GeneralSettings.AvatarUrl)) { BlogConfig.GeneralSettings.AvatarUrl = Url.Action("Avatar", "Assets"); @@ -56,9 +57,20 @@ - @BlogConfig.GeneralSettings.OwnerName - + @if (AuthOptions.Value.Provider == AuthenticationProvider.Local) + { + + @BlogConfig.GeneralSettings.OwnerName + + + } + else + { + @BlogConfig.GeneralSettings.OwnerName + + } diff --git a/src/Moonglade.Web/Pages/Shared/_MonacoLoaderScript.cshtml b/src/Moonglade.Web/Pages/Shared/_MonacoLoaderScript.cshtml index bd44fe68d..aae64f830 100644 --- a/src/Moonglade.Web/Pages/Shared/_MonacoLoaderScript.cshtml +++ b/src/Moonglade.Web/Pages/Shared/_MonacoLoaderScript.cshtml @@ -1,8 +1,9 @@ - + crossorigin="anonymous" + referrerpolicy="no-referrer">