├── src ├── client │ ├── AcBlog.Client.WebAssembly │ │ ├── _Imports.razor │ │ ├── wwwroot │ │ │ ├── appsettings.json │ │ │ ├── build.json │ │ │ ├── icon.png │ │ │ ├── service-worker.js │ │ │ ├── manifest.json │ │ │ └── 404.html │ │ ├── .gitignore │ │ ├── docker │ │ │ └── nginx.conf │ │ ├── Dockerfile │ │ ├── Properties │ │ │ └── launchSettings.json │ │ └── AcBlog.Client.WebAssembly.csproj │ ├── AcBlog.Client.UI │ │ ├── .gitignore │ │ ├── Shared │ │ │ ├── EmptyLayout.razor │ │ │ ├── RedirectTo.razor │ │ │ └── IconNames.cs │ │ ├── Models │ │ │ ├── EventCallbackResult.cs │ │ │ ├── ModelValidation.cs │ │ │ ├── PagingData.cs │ │ │ ├── DataLoadingState.cs │ │ │ └── EmptyAuthenticationStateProvider.cs │ │ ├── Components │ │ │ ├── Container.razor │ │ │ ├── Renderers │ │ │ │ ├── ArticleRenderer.razor │ │ │ │ └── SlidesRenderer.razor │ │ │ ├── ButtonLink.razor │ │ │ ├── MdiIcon.razor │ │ │ └── PageContainer.razor │ │ ├── Pages │ │ │ ├── Search │ │ │ │ ├── _Imports.razor │ │ │ │ └── BaseSearchPage.cs │ │ │ ├── Comments │ │ │ │ ├── _Imports.razor │ │ │ │ └── BaseCommentPage.cs │ │ │ ├── Posts │ │ │ │ ├── _Imports.razor │ │ │ │ ├── BasePostPage.cs │ │ │ │ ├── Notes │ │ │ │ │ └── BaseNotePage.cs │ │ │ │ ├── Slides │ │ │ │ │ └── BaseSlidePage.cs │ │ │ │ ├── Articles │ │ │ │ │ └── BaseArticlePage.cs │ │ │ │ ├── Keywords │ │ │ │ │ ├── BaseKeywordPage.cs │ │ │ │ │ └── Index.razor │ │ │ │ ├── Components │ │ │ │ │ └── Displays │ │ │ │ │ │ ├── PostVisitorDisplay.razor │ │ │ │ │ │ ├── DocumentPreviewDisplay.razor │ │ │ │ │ │ ├── ItemTimeDisplay.razor │ │ │ │ │ │ ├── KeywordDisplay.razor │ │ │ │ │ │ ├── WordCountDisplay.razor │ │ │ │ │ │ ├── ReadTimeDisplay.razor │ │ │ │ │ │ ├── CategoryDisplay.razor │ │ │ │ │ │ └── PostCommentDisplay.razor │ │ │ │ └── Categories │ │ │ │ │ └── BaseCategoryPage.cs │ │ │ ├── Authentication │ │ │ │ └── Logout.razor │ │ │ ├── Pages │ │ │ │ └── BasePagePage.cs │ │ │ ├── Archives │ │ │ │ └── BaseArchivePage.cs │ │ │ └── Settings │ │ │ │ └── BaseSettingsPage.cs │ │ ├── Interops │ │ │ ├── LoadingInfoInterop.cs │ │ │ └── WindowInterop.cs │ │ ├── gulpfile.js │ │ ├── UIComponents │ │ │ └── AntDesignUIComponent.cs │ │ ├── package.json │ │ ├── wwwroot │ │ │ ├── js │ │ │ │ └── interop.js │ │ │ └── css │ │ │ │ ├── app.css │ │ │ │ └── mdi.css │ │ ├── ServiceExtensions.cs │ │ ├── ClientUIComponent.cs │ │ └── _Imports.razor │ ├── AcBlog.Client.Server │ │ ├── .gitignore │ │ ├── wwwroot │ │ │ └── icon.png │ │ ├── Properties │ │ │ ├── serviceDependencies.json │ │ │ ├── serviceDependencies.local.json │ │ │ └── launchSettings.json │ │ ├── build.json │ │ ├── _Imports.razor │ │ ├── appsettings.Development.json │ │ ├── Pages │ │ │ ├── OpenSearchDescription.cshtml │ │ │ ├── _Host.cshtml.cs │ │ │ └── OpenSearchDescription.cshtml.cs │ │ ├── Program.cs │ │ ├── appsettings.json │ │ ├── Utils.cs │ │ ├── Dockerfile │ │ ├── Controllers │ │ │ └── SiteController.cs │ │ └── AcBlog.Client.Server.csproj │ ├── AcBlog.Client.Core │ │ ├── Models │ │ │ └── RuntimeOptions.cs │ │ └── AccessTokenProvider.cs │ └── AcBlog.Client.WebAssembly.Host │ │ ├── build.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── Program.cs │ │ ├── Controllers │ │ ├── SiteController.cs │ │ └── ServerController.cs │ │ ├── AcBlog.Client.WebAssembly.Host.csproj │ │ ├── Utils.cs │ │ └── Properties │ │ └── launchSettings.json ├── AcBlog.Server.Api │ ├── Pages │ │ ├── _ViewStart.cshtml │ │ ├── _ViewImports.cshtml │ │ ├── Error.cshtml.cs │ │ ├── Error.cshtml │ │ └── Shared │ │ │ ├── _LoginPartial.cshtml │ │ │ └── _ValidationScriptsPartial.cshtml │ ├── Areas │ │ └── Identity │ │ │ ├── Pages │ │ │ ├── Account │ │ │ │ └── _ViewImports.cshtml │ │ │ ├── _ViewStart.cshtml │ │ │ ├── _ViewImports.cshtml │ │ │ ├── _ValidationScriptsPartial.cshtml │ │ │ └── Shared │ │ │ │ └── _LoginPartial.cshtml │ │ │ └── IdentityHostingStartup.cs │ ├── Models │ │ ├── AppOptions.cs │ │ └── ApplicationUser.cs │ ├── appsettings.Development.json │ ├── Data │ │ ├── ServiceDbContext.cs │ │ └── IdentityDbContext.cs │ ├── Controllers │ │ ├── PagesController.cs │ │ ├── LayoutsController.cs │ │ ├── CommentsController.cs │ │ ├── StatisticsController.cs │ │ ├── PostsController.cs │ │ ├── OidcConfigurationController.cs │ │ └── BlogController.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Dockerfile │ ├── Program.cs │ └── appsettings.json ├── AcBlog.Tools.Sdk │ ├── Properties │ │ └── launchSettings.json │ ├── Models │ │ ├── Text │ │ │ ├── MetadataBase.cs │ │ │ └── LayoutMetadata.cs │ │ ├── Db.cs │ │ ├── WorkspaceOption.cs │ │ └── RemoteOption.cs │ ├── Repositories │ │ ├── PageService.cs │ │ ├── LayoutService.cs │ │ ├── PostService.cs │ │ ├── PageFSRepo.cs │ │ └── LayoutFSRepo.cs │ ├── Commands │ │ ├── ToolCommand.cs │ │ ├── NewCommand.cs │ │ ├── ListCommand.cs │ │ ├── CleanCommand.cs │ │ ├── InitCommand.cs │ │ └── Remotes │ │ │ └── RemoveCommand.cs │ └── Helpers │ │ ├── Equality.cs │ │ ├── ConsoleExtensions.cs │ │ └── FSExtensions.cs ├── AcBlog.Data │ ├── Models │ │ ├── RHasId.cs │ │ ├── Actions │ │ │ ├── FileQueryRequest.cs │ │ │ ├── QueryStatus.cs │ │ │ ├── UserQueryRequest.cs │ │ │ ├── LayoutQueryRequest.cs │ │ │ ├── QueryRequest.cs │ │ │ ├── PageQueryRequest.cs │ │ │ ├── QueryTimeOrder.cs │ │ │ ├── QueryStatistic.cs │ │ │ ├── StatisticQueryRequest.cs │ │ │ ├── CommentQueryRequest.cs │ │ │ ├── PostQueryRequest.cs │ │ │ ├── QueryResponse.cs │ │ │ └── Pagination.cs │ │ ├── PostType.cs │ │ ├── File.cs │ │ ├── User.cs │ │ ├── Layout.cs │ │ ├── Feature.cs │ │ ├── KeywordCollection.cs │ │ ├── Keyword.cs │ │ ├── Category.cs │ │ ├── Statistic.cs │ │ ├── NameUtility.cs │ │ ├── PropertyCollection.cs │ │ ├── Comment.cs │ │ ├── Page.cs │ │ ├── BlogOptions.cs │ │ ├── Post.cs │ │ ├── Builders │ │ │ ├── FeatureBuilder.cs │ │ │ ├── KeywordBuilder.cs │ │ │ └── CategoryBuilder.cs │ │ └── CategoryTree.cs │ ├── Protections │ │ ├── ProtectionKey.cs │ │ ├── IProtector.cs │ │ └── ProtectionException.cs │ ├── Repositories │ │ ├── IRepository.cs │ │ ├── RepositoryAccessContext.cs │ │ ├── RepositoryStatus.cs │ │ ├── IFileRepository.cs │ │ ├── IPageRepository.cs │ │ ├── ILayoutRepository.cs │ │ ├── ICommentRepository.cs │ │ ├── IStatisticRepository.cs │ │ ├── IUserRepository.cs │ │ └── IPostRepository.cs │ ├── Documents │ │ └── Document.cs │ └── AcBlog.Data.csproj ├── AcBlog.Data.Repositories.SqlServer │ ├── Models │ │ ├── IHasId.cs │ │ ├── RawEntry.cs │ │ ├── BlogDataContext.cs │ │ ├── RawUser.cs │ │ ├── RawLayout.cs │ │ └── RawStatistic.cs │ └── AcBlog.Data.Repositories.SqlServer.csproj ├── AcBlog.Services │ ├── IFileService.cs │ ├── IPageService.cs │ ├── ILayoutService.cs │ ├── ICommentService.cs │ ├── IStatisticService.cs │ ├── Models │ │ ├── UserLoginRequest.cs │ │ ├── UserChangePasswordRequest.cs │ │ └── BlogQueryRequest.cs │ ├── Generators │ │ ├── Sitemap │ │ │ ├── ChangeFrequency.cs │ │ │ └── SitemapUrl.cs │ │ ├── IClientUriGenerator.cs │ │ └── ClientUriGenerator.cs │ ├── IPostService.cs │ ├── Extensions │ │ ├── PageServiceExtensions.cs │ │ ├── FileServiceExtensions.cs │ │ ├── CommentServiceExtensions.cs │ │ ├── StatisticServiceExtensions.cs │ │ ├── PostServiceExtensions.cs │ │ └── BlogServiceExtensions.cs │ ├── IQueryFilter.cs │ ├── IUserService.cs │ ├── Filters │ │ ├── PostKeywordFilter.cs │ │ ├── PostCategoryFilter.cs │ │ ├── PageRouteFilter.cs │ │ └── BaseQueryFilter.cs │ ├── IBlogService.cs │ └── AcBlog.Services.csproj ├── AcBlog.Data.Extensions │ ├── Pages │ │ ├── IPageRenderService.cs │ │ └── IMarkdownRenderService.cs │ ├── Repositories │ │ └── Searchers │ │ │ └── Local │ │ │ ├── LocalFileRepositorySearcher.cs │ │ │ ├── LocalLayoutRepositorySearcher.cs │ │ │ ├── LocalStatisticRepositorySearcher.cs │ │ │ └── LocalPageRepositorySearcher.cs │ ├── PagingData.cs │ ├── Extensions │ │ ├── DocumentExtensions.cs │ │ └── PagingDataExtensions.cs │ └── AcBlog.Data.Extensions.csproj ├── AcBlog.Data.Repositories.FileSystem │ ├── Builders │ │ ├── FileRepositoryBuilder.cs │ │ ├── LayoutRepositoryBuilder.cs │ │ └── PageRepositoryBuilder.cs │ ├── Readers │ │ ├── FileFSReader.cs │ │ └── LayoutFSReader.cs │ └── AcBlog.Data.Repositories.FileSystem.csproj ├── AcBlog.Sdk │ ├── Api │ │ ├── PageService.cs │ │ ├── LayoutService.cs │ │ ├── CommentService.cs │ │ └── StatisticService.cs │ └── AcBlog.Sdk.csproj ├── AcBlog.Services.SqlServer │ ├── CommentService.cs │ ├── StatisticService.cs │ ├── PageService.cs │ ├── LayoutService.cs │ ├── UserService.cs │ ├── PostService.cs │ └── AcBlog.Services.SqlServer.csproj ├── AcBlog.Services.FileSystem │ ├── PageService.cs │ ├── LayoutService.cs │ ├── FileService.cs │ ├── PostService.cs │ └── AcBlog.Services.FileSystem.csproj ├── StardustDL.Extensions.FileProviders.Http │ ├── HttpFileProvider.cs │ ├── HttpFileInfo.cs │ └── StardustDL.Extensions.FileProviders.Http.csproj └── AcBlog.Data.Repositories.Externals │ └── AcBlog.Data.Repositories.Externals.csproj ├── assets └── images │ └── icon.png ├── test ├── Test.Base │ ├── BasicTest.cs │ └── Test.Base.csproj ├── Test.Data │ ├── Protections │ │ └── DocumentProtectorTest.cs │ ├── Models │ │ ├── Actions │ │ │ └── PaginationTest.cs │ │ └── Builders │ │ │ ├── FeatureBuilderTest.cs │ │ │ ├── KeywordBuilderTest.cs │ │ │ └── CategoryBuilderTest.cs │ └── Test.Data.csproj ├── Test.Utils │ ├── Test.Utils.csproj │ └── Data │ │ └── Protections │ │ └── ProtectorTester.cs └── Benchmark.Base │ ├── Benchmark.Base.csproj │ └── Program.cs ├── .dockerignore └── .github └── dependabot.yml /src/client/AcBlog.Client.WebAssembly/_Imports.razor: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/lib 2 | wwwroot/data -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/lib 2 | wwwroot/data -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/lib 2 | wwwroot/data -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/wwwroot/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "Build": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StardustDL/acblog/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Shared/EmptyLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | @Body 4 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Areas/Identity/Pages/Account/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using AcBlog.Server.Api.Areas.Identity.Pages.Account 2 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Areas/Identity/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | 2 | @{ 3 | Layout = "/Pages/Shared/_Layout.cshtml"; 4 | } 5 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/wwwroot/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StardustDL/acblog/HEAD/src/client/AcBlog.Client.Server/wwwroot/icon.png -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "AcBlog.Tools.Sdk": { 4 | "commandName": "Project" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/wwwroot/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StardustDL/acblog/HEAD/src/client/AcBlog.Client.WebAssembly/wwwroot/icon.png -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/RHasId.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models 2 | { 3 | public record RHasId 4 | { 5 | public T? Id { get; init; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/FileQueryRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public record FileQueryRequest : QueryRequest 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/QueryStatus.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public enum QueryStatus 4 | { 5 | Success, 6 | Error, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/PostType.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models 2 | { 3 | public enum PostType 4 | { 5 | Article, 6 | Slides, 7 | Note, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/File.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models 2 | { 3 | public record File : RHasId 4 | { 5 | public string Uri { get; init; } = string.Empty; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Core/Models/RuntimeOptions.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Client.Models 2 | { 3 | public class RuntimeOptions 4 | { 5 | public bool IsPrerender { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.SqlServer/Models/IHasId.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Repositories.SqlServer.Models 2 | { 3 | public interface IHasId 4 | { 5 | T Id { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql", 5 | "connectionId": "DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Models/EventCallbackResult.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Client.UI.Models 2 | { 3 | public class EventCallbackResult 4 | { 5 | public T Result { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Protections/ProtectionKey.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Protections 2 | { 3 | public class ProtectionKey 4 | { 5 | public string Password { get; set; } = string.Empty; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/IRepository.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Repositories 2 | { 3 | public interface IRepository 4 | { 5 | 6 | RepositoryAccessContext Context { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Models/AppOptions.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Server.Api.Models 2 | { 3 | public class AppOptions 4 | { 5 | public bool DisableRegisterNewUser { get; set; } = true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace AcBlog.Server.Api.Models 4 | { 5 | public class ApplicationUser : IdentityUser 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Models/Text/MetadataBase.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Tools.Sdk.Models.Text 2 | { 3 | public abstract class MetadataBase 4 | { 5 | public abstract T ApplyTo(T data); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Components/Container.razor: -------------------------------------------------------------------------------- 1 |
2 | @ChildContent 3 |
4 | 5 | @code { 6 | [Parameter] 7 | public RenderFragment ChildContent { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql.local", 5 | "connectionId": "DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using AcBlog.Server.Api.Areas.Identity 3 | 4 | 5 | AcBlog.Server.Api.Pages 6 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 7 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Search/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using AcBlog.Client.UI.Pages.Posts.Components 2 | @using AcBlog.Client.UI.Interops 3 | @using AcBlog.Client.Helpers 4 | 5 | @using System.Web 6 | 7 | @using AntDesign -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Comments/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using AcBlog.Client.UI.Pages.Comments.Components 2 | @using AcBlog.Client.UI.Interops 3 | @using AcBlog.Client.Helpers 4 | 5 | @using System.Web 6 | 7 | @using AntDesign -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "Build": { 3 | "Commit": "", 4 | "Branch": "master", 5 | "BuildDate": "None", 6 | "Repository": "acblog/acblog", 7 | "Version": "1.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/UserQueryRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public record UserQueryRequest : QueryRequest 4 | { 5 | public string NickName { get; init; } = string.Empty; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/RepositoryAccessContext.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Repositories 2 | { 3 | public sealed class RepositoryAccessContext 4 | { 5 | public string Token { get; set; } = string.Empty; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Models/Db.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Tools.Sdk.Models 4 | { 5 | public class DB 6 | { 7 | public DateTimeOffset LastUpdate { get; set; } = DateTimeOffset.MinValue; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly.Host/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "Build": { 3 | "Commit": "", 4 | "Branch": "master", 5 | "BuildDate": "None", 6 | "Repository": "acblog/acblog", 7 | "Version": "1.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Services/IFileService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Repositories; 2 | 3 | namespace AcBlog.Services 4 | { 5 | public interface IFileService : IFileRepository 6 | { 7 | IBlogService BlogService { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Services/IPageService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Repositories; 2 | 3 | namespace AcBlog.Services 4 | { 5 | public interface IPageService : IPageRepository 6 | { 7 | IBlogService BlogService { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Services/ILayoutService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Repositories; 2 | 3 | namespace AcBlog.Services 4 | { 5 | public interface ILayoutService : ILayoutRepository 6 | { 7 | IBlogService BlogService { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/LayoutQueryRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public record LayoutQueryRequest : QueryRequest 4 | { 5 | public QueryTimeOrder Order { get; init; } = QueryTimeOrder.None; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/AcBlog.Services/ICommentService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Repositories; 2 | 3 | namespace AcBlog.Services 4 | { 5 | public interface ICommentService : ICommentRepository 6 | { 7 | IBlogService BlogService { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/RepositoryStatus.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Repositories 2 | { 3 | public class RepositoryStatus 4 | { 5 | public bool CanRead { get; set; } 6 | 7 | public bool CanWrite { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Services/IStatisticService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Repositories; 2 | 3 | namespace AcBlog.Services 4 | { 5 | public interface IStatisticService : IStatisticRepository 6 | { 7 | IBlogService BlogService { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Documents/Document.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Documents 2 | { 3 | public record Document 4 | { 5 | public string Tag { get; init; } = string.Empty; 6 | 7 | public string Raw { get; init; } = string.Empty; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Models/ModelValidation.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AcBlog.Client.UI.Models 4 | { 5 | public class ModelValidation 6 | { 7 | public List Errors { get; } = new List(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using AcBlog.Client.UI.Pages.Posts.Components 2 | @using AcBlog.Client.UI.Pages.Posts.Components.Displays 3 | @using AcBlog.Client.UI.Interops 4 | @using AcBlog.Client.Helpers 5 | 6 | @using System.Web 7 | 8 | @using AntDesign -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/QueryRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public record QueryRequest 4 | { 5 | public Pagination? Pagination { get; init; } 6 | 7 | public string Term { get; init; } = string.Empty; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Areas/Identity/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using AcBlog.Server.Api.Areas.Identity 3 | @using AcBlog.Server.Api.Areas.Identity.Pages 4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 5 | @using AcBlog.Server.Api.Models 6 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Models/PagingData.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models.Actions; 2 | using AcBlog.Data.Repositories.FileSystem; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AcBlog.Client.UI.Models 8 | { 9 | 10 | } -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/Pages/IPageRenderService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using System.Threading.Tasks; 3 | 4 | namespace AcBlog.Data.Pages 5 | { 6 | public interface IPageRenderService 7 | { 8 | Task Render(Page page, Layout? layout); 9 | } 10 | } -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/IFileRepository.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | 4 | namespace AcBlog.Data.Repositories 5 | { 6 | public interface IFileRepository : IRecordRepository 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/IPageRepository.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | 4 | namespace AcBlog.Data.Repositories 5 | { 6 | public interface IPageRepository : IRecordRepository 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Models/UserLoginRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Services.Models 2 | { 3 | public class UserLoginRequest 4 | { 5 | public string UserName { get; set; } = string.Empty; 6 | 7 | public string Password { get; set; } = string.Empty; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Components/Renderers/ArticleRenderer.razor: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | @code { 6 | [Parameter] 7 | public Document Document { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /test/Test.Base/BasicTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace Test.Base 4 | { 5 | [TestClass] 6 | public class BasicTest 7 | { 8 | [TestMethod] 9 | public void Basic() 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/ILayoutRepository.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | 4 | namespace AcBlog.Data.Repositories 5 | { 6 | public interface ILayoutRepository : IRecordRepository 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/ICommentRepository.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | 4 | namespace AcBlog.Data.Repositories 5 | { 6 | public interface ICommentRepository : IRecordRepository 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/IStatisticRepository.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | 4 | namespace AcBlog.Data.Repositories 5 | { 6 | public interface IStatisticRepository : IRecordRepository 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/docker/nginx.conf: -------------------------------------------------------------------------------- 1 | events { } 2 | http { 3 | include /etc/nginx/mime.types; 4 | 5 | server { 6 | listen 80; 7 | 8 | location / { 9 | root /app; 10 | try_files $uri $uri/ /index.html =404; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/AcBlog.Services/Generators/Sitemap/ChangeFrequency.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Services.Generators.Sitemap 2 | { 3 | public enum ChangeFrequency 4 | { 5 | Always, 6 | Hourly, 7 | Daily, 8 | Weekly, 9 | Monthly, 10 | Yearly, 11 | Never 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/Pages/IMarkdownRenderService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace AcBlog.Data.Pages 4 | { 5 | public interface IMarkdownRenderService 6 | { 7 | Task RenderHtml(string markdown); 8 | 9 | Task RenderPlainText(string markdown); 10 | } 11 | } -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/PageQueryRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public record PageQueryRequest : QueryRequest 4 | { 5 | public string Route { get; init; } = string.Empty; 6 | 7 | public QueryTimeOrder Order { get; init; } = QueryTimeOrder.None; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/QueryTimeOrder.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public enum QueryTimeOrder 4 | { 5 | None, 6 | CreationTimeAscending, 7 | CreationTimeDescending, 8 | ModificationTimeAscending, 9 | ModificationTimeDescending, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Models/UserChangePasswordRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Services.Models 2 | { 3 | public class UserChangePasswordRequest 4 | { 5 | public UserLoginRequest LoginRequest { get; set; } = new UserLoginRequest(); 6 | 7 | public string NewPassword { get; set; } = string.Empty; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Components/Renderers/SlidesRenderer.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | @code { 4 | [Parameter] 5 | public Document Document { get; set; } 6 | 7 | [Parameter] 8 | public string Style { get; set; } = "border: 0;"; 9 | } 10 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | // In development, always fetch from the network and do not enable offline support. 2 | // This is because caching would make development more difficult (changes would not 3 | // be reflected on the first load after each change). 4 | self.addEventListener('fetch', () => { }) 5 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models 2 | { 3 | public record User : RHasId 4 | { 5 | public string Name { get; init; } = string.Empty; 6 | 7 | public string NickName { get; init; } = string.Empty; 8 | 9 | public string Email { get; init; } = string.Empty; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace AcBlog.Data.Repositories 7 | { 8 | public interface IUserRepository : IRecordRepository 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | COPY ./src/client/AcBlog.Client.WebAssembly/docker/nginx.conf /etc/nginx/nginx.conf 3 | COPY ./src/client/AcBlog.Client.WebAssembly/docker/mime.types /etc/nginx/mime.types 4 | COPY ./src/client/AcBlog.Client.WebAssembly/publish/wwwroot /app/ 5 | 6 | VOLUME /app/data 7 | EXPOSE 80 8 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.SqlServer/Models/RawEntry.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | 3 | namespace AcBlog.Data.Repositories.SqlServer.Models 4 | { 5 | public class RawEntry : IHasId 6 | { 7 | public string Id { get; set; } = string.Empty; 8 | 9 | public string Value { get; set; } = string.Empty; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Data/ServiceDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace AcBlog.Server.Api.Data 4 | { 5 | public class ServiceDbContext : DbContext 6 | { 7 | public ServiceDbContext( 8 | DbContextOptions options) : base(options) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/AcBlog.Services/IPostService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Documents; 2 | using AcBlog.Data.Protections; 3 | using AcBlog.Data.Repositories; 4 | 5 | namespace AcBlog.Services 6 | { 7 | public interface IPostService : IPostRepository 8 | { 9 | IBlogService BlogService { get; } 10 | 11 | IProtector Protector { get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Layout.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Data.Models 4 | { 5 | public record Layout : RHasId 6 | { 7 | public string Template { get; init; } = string.Empty; 8 | 9 | public DateTimeOffset CreationTime { get; init; } 10 | 11 | public DateTimeOffset ModificationTime { get; init; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.JSInterop 8 | @using AcBlog.Client.Server 9 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Authentication/Logout.razor: -------------------------------------------------------------------------------- 1 | @page "/authentication/logout" 2 | @inherits BasePage 3 | @inject AccessTokenProvider AccessTokenProvider 4 | 5 | @code{ 6 | protected override async Task OnParametersSetAsync() 7 | { 8 | await AccessTokenProvider.SetToken(""); 9 | NavigationManager.NavigateTo("/"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.FileSystem/Builders/FileRepositoryBuilder.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | 3 | namespace AcBlog.Data.Repositories.FileSystem.Builders 4 | { 5 | public class FileRepositoryBuilder : RecordFSBuilderBase 6 | { 7 | public FileRepositoryBuilder(string rootPath) : base(rootPath) 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.FileSystem/Builders/LayoutRepositoryBuilder.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | 3 | namespace AcBlog.Data.Repositories.FileSystem.Builders 4 | { 5 | public class LayoutRepositoryBuilder : RecordFSBuilderBase 6 | { 7 | public LayoutRepositoryBuilder(string rootPath) : base(rootPath) 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Extensions/PageServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Services.Filters; 2 | 3 | namespace AcBlog.Services.Extensions 4 | { 5 | public static class PageServiceExtensions 6 | { 7 | public static PageRouteFilter CreateRouteFilter(this IPageService service) 8 | { 9 | return new PageRouteFilter(service); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AcBlog", 3 | "short_name": "AcBlog", 4 | "start_url": "./", 5 | "display": "standalone", 6 | "background_color": "#ffffff", 7 | "theme_color": "#03173d", 8 | "icons": [ 9 | { 10 | "src": "icon.png", 11 | "type": "image/png", 12 | "sizes": "512x512" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Feature.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models.Builders; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace AcBlog.Data.Models 7 | { 8 | public record Feature 9 | { 10 | public IList Items { get; init; } = Array.Empty(); 11 | 12 | public static Feature Empty => new Feature(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/QueryStatistic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Data.Models.Actions 4 | { 5 | public record QueryStatistic 6 | { 7 | static readonly Lazy _empty = new Lazy(new QueryStatistic { Count = 0 }); 8 | 9 | public static QueryStatistic Empty() => _empty.Value; 10 | 11 | public int Count { get; init; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Generators/Sitemap/SitemapUrl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Services.Generators.Sitemap 4 | { 5 | public class SitemapUrl 6 | { 7 | public string Url { get; set; } = string.Empty; 8 | public DateTime? Modified { get; set; } 9 | public ChangeFrequency? ChangeFrequency { get; set; } 10 | public double? Priority { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/KeywordCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace AcBlog.Data.Models 5 | { 6 | public class KeywordCollection 7 | { 8 | public KeywordCollection() : this(Array.Empty()) { } 9 | 10 | public KeywordCollection(IList items) => Items = items; 11 | 12 | public IList Items { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Keyword.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models.Builders; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | 7 | namespace AcBlog.Data.Models 8 | { 9 | public record Keyword 10 | { 11 | public IList Items { get; init; } = Array.Empty(); 12 | 13 | public static Keyword Empty => new Keyword(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Components/ButtonLink.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | 4 | 5 | @ChildContent 6 | 7 | 8 | 9 | @code { 10 | [Parameter] 11 | public RenderFragment ChildContent { get; set; } 12 | 13 | [Parameter] 14 | public string Href { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Models/DataLoadingState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Client.UI.Models 4 | { 5 | public enum DataLoadingState 6 | { 7 | Initialized, 8 | Loading, 9 | Success, 10 | Failed 11 | } 12 | 13 | [Flags] 14 | public enum MessageModalButtons 15 | { 16 | Ok = 1, 17 | Cancel = 2, 18 | Yes = 4, 19 | No = 8, 20 | } 21 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE.txt 25 | README.md -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Category.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models.Builders; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | 7 | namespace AcBlog.Data.Models 8 | { 9 | public record Category 10 | { 11 | public IList Items { get; init; } = Array.Empty(); 12 | 13 | public static Category Empty => new Category(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Components/MdiIcon.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | @code { 4 | [Parameter] 5 | public string Type { get; set; } 6 | 7 | RenderFragment InnerIcon; 8 | 9 | protected override void OnParametersSet() 10 | { 11 | InnerIcon = @; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/StatisticQueryRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public record StatisticQueryRequest : QueryRequest 4 | { 5 | public string Category { get; init; } = string.Empty; 6 | 7 | public string Uri { get; init; } = string.Empty; 8 | 9 | public string Payload { get; init; } = string.Empty; 10 | 11 | public QueryTimeOrder Order { get; init; } = QueryTimeOrder.None; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Pages/BasePagePage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Pages 4 | { 5 | public class BasePagePage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | value = string.IsNullOrEmpty(value) ? $"Pages" : $"{value} - Pages"; 12 | base.Title = value; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/BasePostPage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Posts 4 | { 5 | public class BasePostPage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | value = string.IsNullOrEmpty(value) ? $"Posts" : $"{value} - Posts"; 12 | base.Title = value; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/Test.Data/Protections/DocumentProtectorTest.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Protections; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Threading.Tasks; 4 | 5 | namespace Test.Data.Protections 6 | { 7 | [TestClass] 8 | public class DocumentProtectorTest 9 | { 10 | [TestMethod] 11 | public async Task Password() 12 | { 13 | await new DocumentProtector().TestPassword(Generator.GetDocument()); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Search/BaseSearchPage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Search 4 | { 5 | public class BaseSearchPage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | value = string.IsNullOrEmpty(value) ? $"Search" : $"{value} - Search"; 12 | base.Title = value; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Models/WorkspaceOption.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using System.Collections.Generic; 3 | 4 | namespace AcBlog.Tools.Sdk.Models 5 | { 6 | public class WorkspaceOption 7 | { 8 | public string CurrentRemote { get; set; } = string.Empty; 9 | 10 | public Dictionary Remotes { get; set; } = new Dictionary(); 11 | 12 | public PropertyCollection Properties { get; set; } = new PropertyCollection(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Archives/BaseArchivePage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Archives 4 | { 5 | public class BaseArchivePage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | value = string.IsNullOrEmpty(value) ? $"Archives" : $"{value} - Archives"; 12 | base.Title = value; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Statistic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Data.Models 4 | { 5 | public record Statistic : RHasId 6 | { 7 | public string Category { get; init; } = string.Empty; 8 | 9 | public string Uri { get; init; } = string.Empty; 10 | 11 | public string Payload { get; init; } = string.Empty; 12 | 13 | public DateTimeOffset CreationTime { get; init; } 14 | 15 | public DateTimeOffset ModificationTime { get; init; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Areas/Identity/IdentityHostingStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | 3 | [assembly: HostingStartup(typeof(AcBlog.Server.Api.Areas.Identity.IdentityHostingStartup))] 4 | namespace AcBlog.Server.Api.Areas.Identity 5 | { 6 | public class IdentityHostingStartup : IHostingStartup 7 | { 8 | public void Configure(IWebHostBuilder builder) 9 | { 10 | builder.ConfigureServices((context, services) => 11 | { 12 | }); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/AcBlog.Data/Repositories/IPostRepository.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace AcBlog.Data.Repositories 7 | { 8 | public interface IPostRepository : IRecordRepository 9 | { 10 | Task GetCategories(CancellationToken cancellationToken = default); 11 | 12 | Task GetKeywords(CancellationToken cancellationToken = default); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Protections/IProtector.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace AcBlog.Data.Protections 5 | { 6 | public interface IProtector 7 | { 8 | public Task IsProtected(T value, CancellationToken cancellationToken = default); 9 | 10 | public Task Protect(T value, ProtectionKey key, CancellationToken cancellationToken = default); 11 | 12 | public Task Deprotect(T value, ProtectionKey key, CancellationToken cancellationToken = default); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AcBlog.Sdk/Api/PageService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories.Searchers; 4 | using AcBlog.Services; 5 | using System.Net.Http; 6 | 7 | namespace AcBlog.Sdk.Api 8 | { 9 | internal class PageService : BaseRecordApiService, IPageService 10 | { 11 | public PageService(IBlogService blog, HttpClient httpClient) : base(blog, httpClient) 12 | { 13 | } 14 | 15 | protected override string PrepUrl => "/Pages"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AcBlog.Sdk/Api/LayoutService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories.Searchers; 4 | using AcBlog.Services; 5 | using System.Net.Http; 6 | 7 | namespace AcBlog.Sdk.Api 8 | { 9 | internal class LayoutService : BaseRecordApiService, ILayoutService 10 | { 11 | public LayoutService(IBlogService blog, HttpClient httpClient) : base(blog, httpClient) 12 | { 13 | } 14 | 15 | protected override string PrepUrl => "/Layouts"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AcBlog.Sdk/Api/CommentService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories.Searchers; 4 | using AcBlog.Services; 5 | using System.Net.Http; 6 | 7 | namespace AcBlog.Sdk.Api 8 | { 9 | internal class CommentService : BaseRecordApiService, ICommentService 10 | { 11 | public CommentService(IBlogService blog, HttpClient httpClient) : base(blog, httpClient) 12 | { 13 | } 14 | 15 | protected override string PrepUrl => "/Comments"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Controllers/PagesController.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Services; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace AcBlog.Server.Api.Controllers 8 | { 9 | [Route("[controller]")] 10 | [ApiController] 11 | public class PagesController : RecordControllerBase 12 | { 13 | public PagesController(IBlogService service) : base(service.PageService) 14 | { 15 | 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/NameUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace AcBlog.Data.Models 5 | { 6 | public static class NameUtility 7 | { 8 | public static string Encode(string str) 9 | { 10 | return Convert.ToBase64String(Encoding.UTF8.GetBytes(str)).Replace('+', '-').Replace('/', '_'); 11 | } 12 | 13 | public static string Decode(string str) 14 | { 15 | return Encoding.UTF8.GetString(Convert.FromBase64String(str.Replace('-', '+').Replace('_', '/'))); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/AcBlog.Sdk/Api/StatisticService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories.Searchers; 4 | using AcBlog.Services; 5 | using System.Net.Http; 6 | 7 | namespace AcBlog.Sdk.Api 8 | { 9 | internal class StatisticService : BaseRecordApiService, IStatisticService 10 | { 11 | public StatisticService(IBlogService blog, HttpClient httpClient) : base(blog, httpClient) 12 | { 13 | } 14 | 15 | protected override string PrepUrl => "/Statistics"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Controllers/LayoutsController.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Services; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace AcBlog.Server.Api.Controllers 8 | { 9 | [Route("[controller]")] 10 | [ApiController] 11 | public class LayoutsController : RecordControllerBase 12 | { 13 | public LayoutsController(IBlogService service) : base(service.LayoutService) 14 | { 15 | 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Notes/BaseNotePage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Posts.Notes 4 | { 5 | public class BaseNotePage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | if (string.IsNullOrEmpty(value)) 12 | value = $"Notes"; 13 | else 14 | value = $"{value} - Notes"; 15 | base.Title = value; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Interops/LoadingInfoInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System.Threading.Tasks; 3 | 4 | namespace AcBlog.Client.UI.Interops 5 | { 6 | public static class LoadingInfoInterop 7 | { 8 | public static ValueTask Show(IJSRuntime runtime) 9 | { 10 | return runtime.InvokeVoidAsync("acblogInteropLoadingInfoShow"); 11 | } 12 | 13 | public static ValueTask Hide(IJSRuntime runtime) 14 | { 15 | return runtime.InvokeVoidAsync("acblogInteropLoadingInfoHide"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Comments/BaseCommentPage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Comments 4 | { 5 | public class BaseCommentPage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | if (string.IsNullOrEmpty(value)) 12 | value = $"Comments"; 13 | else 14 | value = $"{value} - Comments"; 15 | base.Title = value; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Slides/BaseSlidePage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Posts.Slides 4 | { 5 | public class BaseSlidePage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | if (string.IsNullOrEmpty(value)) 12 | value = $"Slides"; 13 | else 14 | value = $"{value} - Slides"; 15 | base.Title = value; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Settings/BaseSettingsPage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Settings 4 | { 5 | public class BaseSettingsPage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | if (string.IsNullOrEmpty(value)) 12 | value = $"Settings"; 13 | else 14 | value = $"{value} - Settings"; 15 | base.Title = value; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/CommentQueryRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public record CommentQueryRequest : QueryRequest 4 | { 5 | public string Content { get; init; } = string.Empty; 6 | 7 | public string Author { get; init; } = string.Empty; 8 | 9 | public string Email { get; init; } = string.Empty; 10 | 11 | public string Link { get; init; } = string.Empty; 12 | 13 | public string Uri { get; init; } = string.Empty; 14 | 15 | public QueryTimeOrder Order { get; init; } = QueryTimeOrder.None; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Shared/RedirectTo.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager Navigation 2 | 3 | @code { 4 | [Parameter] 5 | public string Target { get; set; } 6 | 7 | [Parameter] 8 | public bool HasReturnUrl { get; set; } = false; 9 | 10 | protected override void OnInitialized() 11 | { 12 | if (HasReturnUrl) 13 | { 14 | Navigation.NavigateTo($"{Target}?returnUrl=" + 15 | Uri.EscapeDataString(Navigation.Uri)); 16 | } 17 | else 18 | { 19 | Navigation.NavigateTo(Target); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly.Host/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "Server": { 10 | /*"Identity": { 11 | "Enable": false 12 | }, 13 | "Main": { 14 | "Uri": "" 15 | }, 16 | "Statistic": { 17 | "Uri": "http://localhost:4500", 18 | "Type": 2 19 | }, 20 | "Comment": { 21 | "Uri": "http://localhost:4000", 22 | "Type": 2 23 | }*/ 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/Test.Utils/Test.Utils.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | Test 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Articles/BaseArticlePage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Posts.Articles 4 | { 5 | public class BaseArticlePage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | if (string.IsNullOrEmpty(value)) 12 | value = $"Articles"; 13 | else 14 | value = $"{value} - Articles"; 15 | base.Title = value; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Keywords/BaseKeywordPage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Posts.Keywords 4 | { 5 | public class BaseKeywordPage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | if (string.IsNullOrEmpty(value)) 12 | value = $"Keywords"; 13 | else 14 | value = $"{value} - Keywords"; 15 | base.Title = value; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Components/Displays/PostVisitorDisplay.razor: -------------------------------------------------------------------------------- 1 | @inject IBlogService Service 2 | @inject IClientUriGenerator UrlGenerator 3 | 4 | @if (Count is not null) 5 | { 6 | 7 | @Count.Value 8 | 9 | } 10 | 11 | @code { 12 | [Parameter] 13 | public Post Data { get; set; } 14 | 15 | private long? Count { get; set; } 16 | 17 | protected override async Task OnParametersSetAsync() 18 | { 19 | Count = await Service.StatisticService?.Count(UrlGenerator, Data); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Repositories/PageService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using AcBlog.Data.Repositories; 5 | using AcBlog.Data.Repositories.Searchers; 6 | using AcBlog.Sdk; 7 | using AcBlog.Services; 8 | 9 | namespace AcBlog.Tools.Sdk.Repositories 10 | { 11 | internal class PageService : RecordRepoBasedService, IPageService 12 | { 13 | public PageService(IBlogService blog, string rootPath) : base(blog, new PageFSRepo(rootPath)) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Categories/BaseCategoryPage.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.UI.Shared; 2 | 3 | namespace AcBlog.Client.UI.Pages.Posts.Categories 4 | { 5 | public class BaseCategoryPage : BasePage 6 | { 7 | protected override string Title 8 | { 9 | get => base.Title; set 10 | { 11 | if (string.IsNullOrEmpty(value)) 12 | value = $"Categories"; 13 | else 14 | value = $"{value} - Categories"; 15 | base.Title = value; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AcBlog.Services.SqlServer/CommentService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Data.Repositories.SqlServer; 5 | using AcBlog.Data.Repositories.SqlServer.Models; 6 | 7 | namespace AcBlog.Services.SqlServer 8 | { 9 | internal class CommentService : RecordRepoBasedService, ICommentService 10 | { 11 | public CommentService(IBlogService blog, BlogDataContext dataContext) : base(blog, new CommentDBRepository(dataContext)) 12 | { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | }, 10 | "Server": { 11 | "Identity": { 12 | "Enable": false 13 | } , 14 | "Main": { 15 | "Uri": "" 16 | }/*, 17 | "Statistic": { 18 | "Uri": "http://localhost:4500", 19 | "Type": 2 20 | }, 21 | "Comment": { 22 | "Uri": "http://localhost:4000", 23 | "Type": 2 24 | }*/ 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Components/Displays/DocumentPreviewDisplay.razor: -------------------------------------------------------------------------------- 1 | @Preview 2 | 3 | @code { 4 | [Parameter] 5 | public Document Document { get; set; } 6 | 7 | private string Preview { get; set; } = string.Empty; 8 | 9 | // static MarkdownPipeline Pipeline { get; } = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); 10 | 11 | protected override void OnParametersSet() 12 | { 13 | if (Document is not null) 14 | { 15 | Preview = Document.GetPreview(1000); 16 | } 17 | base.OnParametersSet(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/PostQueryRequest.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public record PostQueryRequest : QueryRequest 4 | { 5 | public PostType? Type { get; init; } 6 | 7 | public string Author { get; init; } = string.Empty; 8 | 9 | public string Content { get; init; } = string.Empty; 10 | 11 | public string Title { get; init; } = string.Empty; 12 | 13 | public Keyword? Keywords { get; init; } 14 | 15 | public Category? Category { get; init; } 16 | 17 | public QueryTimeOrder Order { get; init; } = QueryTimeOrder.None; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Repositories/LayoutService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using AcBlog.Data.Repositories; 5 | using AcBlog.Data.Repositories.Searchers; 6 | using AcBlog.Sdk; 7 | using AcBlog.Services; 8 | 9 | namespace AcBlog.Tools.Sdk.Repositories 10 | { 11 | internal class LayoutService : RecordRepoBasedService, ILayoutService 12 | { 13 | public LayoutService(IBlogService blog, string rootPath) : base(blog, new LayoutFSRepo(rootPath)) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Components/Displays/ItemTimeDisplay.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | @code { 10 | [Parameter] 11 | public DateTimeOffset CreationTime { get; set; } 12 | 13 | [Parameter] 14 | public DateTimeOffset ModificationTime { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Components/Displays/KeywordDisplay.razor: -------------------------------------------------------------------------------- 1 | @inject IBlogService Service 2 | @inject IClientUriGenerator UrlGenerator 3 | 4 | 5 | 6 | @foreach (var key in Data.Items) 7 | { 8 | @key 9 | ; 10 | } 11 | 12 | 13 | 14 | 15 | @code { 16 | [Parameter] 17 | public Keyword Data { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /test/Benchmark.Base/Benchmark.Base.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/AcBlog.Services.SqlServer/StatisticService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Data.Repositories.SqlServer; 5 | using AcBlog.Data.Repositories.SqlServer.Models; 6 | 7 | namespace AcBlog.Services.SqlServer 8 | { 9 | internal class StatisticService : RecordRepoBasedService, IStatisticService 10 | { 11 | public StatisticService(IBlogService blog, BlogDataContext dataContext) : base(blog, new StatisticDBRepository(dataContext)) 12 | { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Protections/ProtectionException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Data.Protections 4 | { 5 | [Serializable] 6 | public class ProtectionException : Exception 7 | { 8 | public ProtectionException() { } 9 | public ProtectionException(string message) : base(message) { } 10 | public ProtectionException(string message, Exception inner) : base(message, inner) { } 11 | protected ProtectionException( 12 | System.Runtime.Serialization.SerializationInfo info, 13 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Components/Displays/WordCountDisplay.razor: -------------------------------------------------------------------------------- 1 | 2 | @Count 3 | 4 | 5 | @code { 6 | [Parameter] 7 | public Document Document { get; set; } 8 | 9 | private string Count { get; set; } 10 | 11 | protected override void OnParametersSet() 12 | { 13 | int len = Document.CountWords(); 14 | if (len < 1000) 15 | { 16 | Count = len.ToString(); 17 | } 18 | else 19 | { 20 | Count = $"{(len / 1000.0).ToString("f1")}k"; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Models/EmptyAuthenticationStateProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Authorization; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | 5 | namespace AcBlog.Client.UI.Models 6 | { 7 | public class EmptyAuthenticationStateProvider : AuthenticationStateProvider 8 | { 9 | static AuthenticationState State { get; set; } = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); 10 | 11 | public override Task GetAuthenticationStateAsync() 12 | { 13 | return Task.FromResult(State); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/PropertyCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AcBlog.Data.Models 4 | { 5 | public class PropertyCollection 6 | { 7 | public PropertyCollection() : this(new Dictionary()) { } 8 | 9 | public PropertyCollection(Dictionary raw) => Raw = raw; 10 | 11 | public Dictionary Raw { get; set; } 12 | 13 | public string this[string key, string defaultValue = ""] 14 | { 15 | get => Raw.TryGetValue(key, out var res) ? res : defaultValue; 16 | set => Raw[key] = value; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Generators/IClientUriGenerator.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | 3 | namespace AcBlog.Services.Generators 4 | { 5 | public interface IClientUriGenerator 6 | { 7 | string Search(string query = ""); 8 | string Archives(); 9 | string Articles(); 10 | string Categories(); 11 | string Category(Category category); 12 | string Keyword(Keyword keyword); 13 | string Keywords(); 14 | string Notes(); 15 | string Page(Page page); 16 | string Post(Post post); 17 | string Posts(); 18 | string Slides(); 19 | string Comments(); 20 | } 21 | } -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Models/RemoteOption.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | 3 | namespace AcBlog.Tools.Sdk.Models 4 | { 5 | public enum RemoteType 6 | { 7 | LocalFS, 8 | RemoteFS, 9 | Api, 10 | Git 11 | } 12 | 13 | public class RemoteOption 14 | { 15 | public string Name { get; set; } = string.Empty; 16 | 17 | public string Uri { get; set; } = string.Empty; 18 | 19 | public string Token { get; set; } = string.Empty; 20 | 21 | public PropertyCollection Properties { get; set; } = new PropertyCollection(); 22 | 23 | public RemoteType Type { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Pages/OpenSearchDescription.cshtml: -------------------------------------------------------------------------------- 1 | @page "/Site/opensearch" 2 | @model AcBlog.Client.Server.Pages.OpenSearchDescriptionModel 3 | @{ 4 | Layout = null; 5 | Response.ContentType = "text/xml"; 6 | } 7 | 8 | 9 | @Model.BlogOptions.Name 10 | @Model.BlogOptions.Description 11 | @Model.BlogOptions.Icon 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/gulpfile.js: -------------------------------------------------------------------------------- 1 | /// 2 | 'use strict' 3 | 4 | var gulp = require('gulp') 5 | 6 | gulp.task('jquery', function () { 7 | return gulp.src('./node_modules/jquery/dist/*.min.*').pipe(gulp.dest('wwwroot/lib/jquery')) 8 | }) 9 | 10 | gulp.task('bootstrap', function () { 11 | return gulp.src('./node_modules/bootstrap/dist/**/*.min.*').pipe(gulp.dest('wwwroot/lib/bootstrap')) 12 | }) 13 | 14 | gulp.task('markdown-css', function () { 15 | return gulp.src('./node_modules/github-markdown-css/*.css').pipe(gulp.dest('wwwroot/lib/markdown-css')) 16 | }) 17 | 18 | gulp.task('default', gulp.parallel('bootstrap', 'jquery', 'markdown-css')) 19 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Data/IdentityDbContext.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Server.Api.Models; 2 | using IdentityServer4.EntityFramework.Options; 3 | using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace AcBlog.Server.Api.Data 8 | { 9 | public class IdentityDbContext : ApiAuthorizationDbContext 10 | { 11 | public IdentityDbContext( 12 | DbContextOptions options, 13 | IOptions operationalStoreOptions) : base(options, operationalStoreOptions) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AcBlog.Services.SqlServer/PageService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using AcBlog.Data.Repositories; 5 | using AcBlog.Data.Repositories.Searchers; 6 | using AcBlog.Data.Repositories.SqlServer; 7 | using AcBlog.Data.Repositories.SqlServer.Models; 8 | 9 | namespace AcBlog.Services.SqlServer 10 | { 11 | internal class PageService : RecordRepoBasedService, IPageService 12 | { 13 | public PageService(IBlogService blog, BlogDataContext dataContext) : base(blog, new PageDBRepository(dataContext)) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Components/Displays/ReadTimeDisplay.razor: -------------------------------------------------------------------------------- 1 | 2 | @Time 3 | 4 | 5 | @code { 6 | const int WordPerMinute = 500; 7 | 8 | [Parameter] 9 | public Document Document { get; set; } 10 | 11 | private string Time { get; set; } 12 | 13 | protected override void OnParametersSet() 14 | { 15 | int time = (int)Document.GetReadTime().TotalMinutes; 16 | if (time <= 1) 17 | { 18 | Time = "1 minute"; 19 | } 20 | else 21 | { 22 | Time = $"{time.ToString()} minutes"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/UIComponents/AntDesignUIComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace AcBlog.UI.Components 4 | { 5 | public class AntDesignUIComponent : UIComponent 6 | { 7 | public AntDesignUIComponent() 8 | { 9 | AddStyleSheetResource("_content/AntDesign/css/ant-design-blazor.css"); 10 | AddScriptResource("_content/AntDesign/js/ant-design-blazor.js"); 11 | } 12 | 13 | public override void ConfigureServices(IServiceCollection services) 14 | { 15 | services.AddAntDesign(); 16 | base.ConfigureServices(services); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AcBlog.Services.SqlServer/LayoutService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using AcBlog.Data.Repositories; 5 | using AcBlog.Data.Repositories.Searchers; 6 | using AcBlog.Data.Repositories.SqlServer; 7 | using AcBlog.Data.Repositories.SqlServer.Models; 8 | 9 | namespace AcBlog.Services.SqlServer 10 | { 11 | internal class LayoutService : RecordRepoBasedService, ILayoutService 12 | { 13 | public LayoutService(IBlogService blog, BlogDataContext dataContext) : base(blog, new LayoutDBRepository(dataContext)) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AcBlog.Services.FileSystem/PageService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using AcBlog.Data.Repositories; 5 | using AcBlog.Data.Repositories.FileSystem.Readers; 6 | using AcBlog.Data.Repositories.Searchers; 7 | using StardustDL.Extensions.FileProviders; 8 | 9 | namespace AcBlog.Services.FileSystem 10 | { 11 | internal class PageService : RecordRepoBasedService, IPageService 12 | { 13 | public PageService(IBlogService blog, string rootPath, IFileProvider fileProvider) : base(blog, new PageFSReader(rootPath, fileProvider)) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AcBlog.Services/IQueryFilter.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data; 2 | using AcBlog.Data.Models.Actions; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace AcBlog.Services 7 | { 8 | public interface IQueryFilter 9 | { 10 | TService BaseService { get; } 11 | 12 | Task> Filter(Pagination? pagination, QueryTimeOrder order = QueryTimeOrder.None); 13 | } 14 | 15 | public interface IQueryFilter 16 | { 17 | TService BaseService { get; } 18 | 19 | Task> Filter(T arg, Pagination? pagination, QueryTimeOrder order = QueryTimeOrder.None); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Commands/ToolCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | 3 | namespace AcBlog.Tools.Sdk.Commands 4 | { 5 | public class ToolCommand : BaseCommand 6 | { 7 | public override string Name => "tool"; 8 | 9 | public override string Description => "Tools"; 10 | 11 | protected override bool DisableHandler => true; 12 | 13 | public override Command Configure() 14 | { 15 | var result = base.Configure(); 16 | result.AddCommand(new Tools.CompleteCommand().Build()); 17 | return result; 18 | } 19 | 20 | public class CArgument 21 | { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Comment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Data.Models 4 | { 5 | public record Comment : RHasId 6 | { 7 | public string Content { get; init; } = string.Empty; 8 | 9 | public string Author { get; init; } = string.Empty; 10 | 11 | public string Email { get; init; } = string.Empty; 12 | 13 | public string Link { get; init; } = string.Empty; 14 | 15 | public string Extra { get; init; } = string.Empty; 16 | 17 | public DateTimeOffset CreationTime { get; init; } 18 | 19 | public DateTimeOffset ModificationTime { get; init; } 20 | 21 | public string Uri { get; init; } = string.Empty; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Commands/NewCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | 3 | namespace AcBlog.Tools.Sdk.Commands 4 | { 5 | public class NewCommand : BaseCommand 6 | { 7 | public override string Name => "new"; 8 | 9 | public override string Description => "Create blog contents."; 10 | 11 | protected override bool DisableHandler => true; 12 | 13 | public override Command Configure() 14 | { 15 | var result = base.Configure(); 16 | result.AddCommand(new News.PostCommand().Build()); 17 | return result; 18 | } 19 | 20 | public class CArgument 21 | { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Commands/ListCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | 3 | namespace AcBlog.Tools.Sdk.Commands 4 | { 5 | public class ListCommand : BaseCommand 6 | { 7 | public override string Name => "list"; 8 | 9 | public override string Description => "List blog contents."; 10 | 11 | protected override bool DisableHandler => true; 12 | 13 | public override Command Configure() 14 | { 15 | var result = base.Configure(); 16 | result.AddCommand(new Lists.PostCommand().Build()); 17 | return result; 18 | } 19 | 20 | public class CArgument 21 | { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/AcBlog.Services.FileSystem/LayoutService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using AcBlog.Data.Repositories; 5 | using AcBlog.Data.Repositories.FileSystem.Readers; 6 | using AcBlog.Data.Repositories.Searchers; 7 | using StardustDL.Extensions.FileProviders; 8 | 9 | namespace AcBlog.Services.FileSystem 10 | { 11 | internal class LayoutService : RecordRepoBasedService, ILayoutService 12 | { 13 | public LayoutService(IBlogService blog, string rootPath, IFileProvider fileProvider) : base(blog, new LayoutFSReader(rootPath, fileProvider)) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AcBlog.Services/IUserService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Services.Models; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace AcBlog.Services 9 | { 10 | public interface IUserService : IUserRepository 11 | { 12 | IBlogService BlogService { get; } 13 | 14 | Task GetCurrent(CancellationToken cancellationToken = default); 15 | 16 | Task Login(UserLoginRequest request, CancellationToken cancellationToken = default); 17 | 18 | Task ChangePassword(UserChangePasswordRequest request, CancellationToken cancellationToken = default); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AcBlog.Services.FileSystem/FileService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using AcBlog.Data.Repositories; 5 | using AcBlog.Data.Repositories.FileSystem.Readers; 6 | using AcBlog.Data.Repositories.Searchers; 7 | using AcBlog.Services; 8 | using StardustDL.Extensions.FileProviders; 9 | 10 | namespace AcBlog.Services.FileSystem 11 | { 12 | internal class FileService : RecordRepoBasedService, IFileService 13 | { 14 | public FileService(IBlogService blog, string rootPath, IFileProvider fileProvider) : base(blog, new FileFSReader(rootPath, fileProvider)) 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Page.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Data.Models 4 | { 5 | public record Page : RHasId 6 | { 7 | public string Layout { get; init; } = string.Empty; 8 | 9 | public string Content { get; init; } = string.Empty; 10 | 11 | public string Route { get; init; } = string.Empty; 12 | 13 | public string Title { get; init; } = string.Empty; 14 | 15 | public Feature Features { get; init; } = Feature.Empty; 16 | 17 | public PropertyCollection Properties { get; init; } = new PropertyCollection(); 18 | 19 | public DateTimeOffset CreationTime { get; init; } 20 | 21 | public DateTimeOffset ModificationTime { get; init; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/BlogOptions.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models 2 | { 3 | public class BlogOptions 4 | { 5 | public string Name { get; set; } = "AcBlog"; 6 | 7 | public string Description { get; set; } = "A blog system based on WebAssembly."; 8 | 9 | public int StartYear { get; set; } = 2020; 10 | 11 | public string Onwer { get; set; } = "StardustDL"; 12 | 13 | public string Icon { get; set; } = "icon.png"; 14 | 15 | public string Cover { get; set; } = ""; 16 | 17 | public string Header { get; set; } = ""; 18 | 19 | public string Footer { get; set; } = ""; 20 | 21 | public PropertyCollection Properties { get; set; } = new PropertyCollection(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Components/Displays/CategoryDisplay.razor: -------------------------------------------------------------------------------- 1 | @inject IBlogService Service 2 | @inject IClientUriGenerator UrlGenerator 3 | 4 | @using AcBlog.Data.Models.Builders 5 | 6 | 7 | 8 | @for (int i = 0; i < Data.Items.Count; i++) 9 | { 10 | CategoryBuilder builder = new CategoryBuilder(); 11 | builder.AddSubCategory(Data.Items.Take(i + 1).ToArray()); 12 | @Data.Items[i] 13 | / 14 | } 15 | 16 | 17 | 18 | @code { 19 | [Parameter] 20 | public Category Data { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Post.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Documents; 2 | using System; 3 | 4 | namespace AcBlog.Data.Models 5 | { 6 | public record Post : RHasId 7 | { 8 | public PostType Type { get; init; } = PostType.Article; 9 | 10 | public string Author { get; init; } = string.Empty; 11 | 12 | public Document Content { get; init; } = new Document(); 13 | 14 | public string Title { get; init; } = string.Empty; 15 | 16 | public Category Category { get; init; } = Category.Empty; 17 | 18 | public Keyword Keywords { get; init; } = Keyword.Empty; 19 | 20 | public DateTimeOffset CreationTime { get; init; } 21 | 22 | public DateTimeOffset ModificationTime { get; init; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/StardustDL.Extensions.FileProviders.Http/HttpFileProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace StardustDL.Extensions.FileProviders.Http 6 | { 7 | public class HttpFileProvider : IFileProvider 8 | { 9 | public HttpFileProvider(HttpClient client) => Client = client; 10 | 11 | public HttpClient Client { get; } 12 | 13 | public Task GetDirectoryContents(string subpath) => throw new NotImplementedException(); 14 | 15 | public async Task GetFileInfo(string subpath) 16 | { 17 | var result = await Client.GetAsync(subpath).ConfigureAwait(false); 18 | return new HttpFileInfo(result); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/Benchmark.Base/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Columns; 2 | using BenchmarkDotNet.Configs; 3 | using BenchmarkDotNet.Diagnosers; 4 | using BenchmarkDotNet.Order; 5 | using BenchmarkDotNet.Running; 6 | 7 | namespace Benchmark.Base 8 | { 9 | public partial class Program 10 | { 11 | static void Main(string[] args) 12 | { 13 | var config = DefaultConfig.Instance 14 | .WithOptions(ConfigOptions.JoinSummary) 15 | .AddColumn(CategoriesColumn.Default) 16 | .AddDiagnoser(MemoryDiagnoser.Default) 17 | .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest)); 18 | _ = BenchmarkRunner.Run(typeof(Program).Assembly, config); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "acblog.client.ui", 3 | "version": "1.0.0", 4 | "description": "AcBlog", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "StardustDL", 9 | "license": "ISC", 10 | "private": true, 11 | "dependencies": { 12 | "bootstrap": "^4.5.3", 13 | "github-markdown-css": "^4.0.0", 14 | "jquery": "^3.5.1", 15 | "monaco-editor": "^0.21.2", 16 | "popper.js": "^1.16.1" 17 | }, 18 | "devDependencies": { 19 | "browserify": "^17.0.0", 20 | "gulp": "^4.0.2", 21 | "gulp-sourcemaps": "^3.0.0", 22 | "gulp-uglify": "^3.0.2", 23 | "gulplog": "^1.0.0", 24 | "vinyl-buffer": "^1.0.1", 25 | "vinyl-source-stream": "^2.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/Test.Base/Test.Base.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/QueryResponse.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Data.Models.Actions 2 | { 3 | public record QueryResponse 4 | { 5 | public QueryStatus Status { get; init; } 6 | 7 | public T? Result { get; init; } 8 | } 9 | 10 | public static class QueryResponse 11 | { 12 | public static QueryResponse Error() 13 | { 14 | return new QueryResponse 15 | { 16 | Status = QueryStatus.Error 17 | }; 18 | } 19 | 20 | public static QueryResponse Success(T value) 21 | { 22 | return new QueryResponse 23 | { 24 | Status = QueryStatus.Success, 25 | Result = value, 26 | }; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Extensions/FileServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | 5 | namespace AcBlog.Services.Extensions 6 | { 7 | public static class FileServiceExtensions 8 | { 9 | public static IFileService AsService(this IFileRepository repository, IBlogService blogService) 10 | { 11 | return new RepoBasedService(blogService, repository); 12 | } 13 | 14 | class RepoBasedService : RecordRepoBasedService, IFileService 15 | { 16 | public RepoBasedService(IBlogService blogService, IFileRepository repository) : base(blogService, repository) 17 | { 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 3 8 | - package-ecosystem: npm 9 | directory: "/src/client/AcBlog.Client.UI" 10 | schedule: 11 | interval: daily 12 | allow: 13 | - dependency-type: production 14 | open-pull-requests-limit: 3 15 | - package-ecosystem: "docker" 16 | directory: "/src/client/AcBlog.Client.WebAssembly" 17 | schedule: 18 | interval: daily 19 | - package-ecosystem: "docker" 20 | directory: "/src/client/AcBlog.Client.WebAssembly.Host" 21 | schedule: 22 | interval: daily 23 | - package-ecosystem: "docker" 24 | directory: "/src/AcBlog.Server.Api" 25 | schedule: 26 | interval: daily 27 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.SqlServer/Models/BlogDataContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace AcBlog.Data.Repositories.SqlServer.Models 4 | { 5 | public class BlogDataContext : DbContext 6 | { 7 | public BlogDataContext(DbContextOptions options) : base(options) 8 | { 9 | } 10 | 11 | public DbSet Posts { get; set; } 12 | 13 | public DbSet Pages { get; set; } 14 | 15 | public DbSet Layouts { get; set; } 16 | 17 | public DbSet Comments { get; set; } 18 | 19 | public DbSet Statistics { get; set; } 20 | 21 | public DbSet Users { get; set; } 22 | 23 | public DbSet BlogEntries { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Controllers/CommentsController.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Services; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Server.Api.Controllers 10 | { 11 | [Route("[controller]")] 12 | [ApiController] 13 | public class CommentsController : RecordControllerBase 14 | { 15 | public CommentsController(IBlogService service) : base(service.CommentService) 16 | { 17 | 18 | } 19 | 20 | [AllowAnonymous] 21 | public override Task> Create([FromBody] Comment value) => base.Create(value); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/Repositories/Searchers/Local/LocalFileRepositorySearcher.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Data.Repositories.Searchers.Local 10 | { 11 | public class LocalFileRepositorySearcher : IFileRepositorySearcher 12 | { 13 | public IAsyncEnumerable Search(IFileRepository repository, FileQueryRequest query, CancellationToken cancellationToken = default) 14 | { 15 | var qr = repository.GetAllItems(cancellationToken).IgnoreNull(); 16 | 17 | return qr.Select(item => item.Id).IgnoreNull().Paging(query.Pagination); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/wwwroot/js/interop.js: -------------------------------------------------------------------------------- 1 | window.acblogInteropSetTitle = function (newTitle) { 2 | document.title = newTitle 3 | } 4 | window.acblogInteropScrollTo = function (id) { 5 | var scrollToElement = document.getElementById(id) 6 | if (scrollToElement && scrollToElement.offsetTop) { 7 | window.scrollTo(0, scrollToElement.offsetTop) 8 | } 9 | } 10 | window.acblogInteropCopyItem = function (ele) { 11 | ele.select() 12 | document.execCommand('copy') 13 | } 14 | window.acblogInteropLoadingInfoShow = function () { 15 | var loadingInfo = document.getElementById('loading-info-ui') 16 | loadingInfo.style = 'display: block' 17 | } 18 | window.acblogInteropLoadingInfoHide = function () { 19 | var loadingInfo = document.getElementById('loading-info-ui') 20 | loadingInfo.style = 'display: none' 21 | } 22 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/Repositories/Searchers/Local/LocalLayoutRepositorySearcher.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Data.Repositories.Searchers.Local 10 | { 11 | public class LocalLayoutRepositorySearcher : ILayoutRepositorySearcher 12 | { 13 | public IAsyncEnumerable Search(ILayoutRepository repository, LayoutQueryRequest query, CancellationToken cancellationToken = default) 14 | { 15 | var qr = repository.GetAllItems(cancellationToken).IgnoreNull(); 16 | 17 | return qr.Select(item => item.Id).IgnoreNull().Paging(query.Pagination); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Controllers/StatisticsController.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Services; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Server.Api.Controllers 10 | { 11 | [Route("[controller]")] 12 | [ApiController] 13 | public class StatisticsController : RecordControllerBase 14 | { 15 | public StatisticsController(IBlogService service) : base(service.StatisticService) 16 | { 17 | 18 | } 19 | 20 | [AllowAnonymous] 21 | public override Task> Create([FromBody] Statistic value) => base.Create(value); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | @import "mdi.css"; 2 | 3 | html { 4 | position: relative; 5 | min-height: 100%; 6 | } 7 | 8 | .valid.modified:not([type=checkbox]) { 9 | outline: 1px solid #26b050; 10 | } 11 | 12 | .invalid { 13 | outline: 1px solid red; 14 | } 15 | 16 | .validation-message { 17 | color: red; 18 | } 19 | 20 | #loading-info-ui { 21 | bottom: 0; 22 | display: none; 23 | left: 0; 24 | position: fixed; 25 | margin-bottom: 0; 26 | width: 100%; 27 | z-index: 1000; 28 | } 29 | 30 | #blazor-error-ui { 31 | bottom: 0; 32 | display: none; 33 | left: 0; 34 | position: fixed; 35 | margin-bottom: 0; 36 | width: 100%; 37 | z-index: 1000; 38 | } 39 | 40 | #blazor-error-ui .dismiss { 41 | cursor: pointer; 42 | position: absolute; 43 | right: 0.75rem; 44 | top: 0.5rem; 45 | } 46 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/PagingData.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models.Actions; 2 | using System.Collections.Generic; 3 | 4 | namespace AcBlog.Data 5 | { 6 | public class PagingData 7 | { 8 | public PagingData(IList results) 9 | { 10 | Results = results; 11 | CurrentPage = new Pagination 12 | { 13 | CurrentPage = 0, 14 | PageSize = results.Count, 15 | TotalCount = results.Count 16 | }; 17 | } 18 | 19 | public PagingData(IEnumerable results, Pagination currentPage) 20 | { 21 | Results = results; 22 | CurrentPage = currentPage; 23 | } 24 | 25 | public IEnumerable Results { get; set; } 26 | 27 | public Pagination CurrentPage { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Models/BlogQueryRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace AcBlog.Services.Models 6 | { 7 | public class BlogQueryRequest 8 | { 9 | public string Type { get; set; } = string.Empty; 10 | 11 | public Dictionary Data { get; set; } = new Dictionary(); 12 | } 13 | 14 | public static class BlogQueryRequestStrings 15 | { 16 | public static string? GetBaseAddress(this BlogQueryRequest request) 17 | { 18 | return request.Data.GetValueOrDefault(BaseAddress); 19 | } 20 | 21 | public const string AtomFeed = "atom"; 22 | 23 | public const string Sitemap = "sitemap"; 24 | 25 | public const string BaseAddress = "baseAddress"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace AcBlog.Client.Server 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureAppConfiguration((host, config) => 17 | { 18 | config.AddJsonFile("build.json"); 19 | }) 20 | .ConfigureWebHostDefaults(webBuilder => 21 | { 22 | webBuilder.UseStartup(); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Interops/WindowInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.JSInterop; 3 | using System.Threading.Tasks; 4 | 5 | namespace AcBlog.Client.UI.Interops 6 | { 7 | public static class WindowInterop 8 | { 9 | public static ValueTask SetTitle(IJSRuntime runtime, string title) 10 | { 11 | return runtime.InvokeVoidAsync("acblogInteropSetTitle", title); 12 | } 13 | 14 | public static ValueTask ScrollTo(IJSRuntime runtime, string title) 15 | { 16 | return runtime.InvokeVoidAsync("acblogInteropScrollTo", title); 17 | } 18 | 19 | public static ValueTask CopyItem(IJSRuntime runtime, ElementReference element) 20 | { 21 | return runtime.InvokeVoidAsync("acblogInteropCopyItem", element); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly.Host/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "BaseAddress": "https://localhost:8101", 11 | "Server": { 12 | "Identity": { 13 | "Enable": true, 14 | "Endpoint": "https://localhost:8001/_configuration/AcBlog.Client.Server", 15 | "RemoteRegisterPath": "https://localhost:8001/Identity/Account/Register", 16 | "RemoteProfilePath": "https://localhost:8001/Identity/Account/Manage" 17 | }, 18 | "Main": { 19 | "Uri": "https://localhost:8001", 20 | "IsStatic": false 21 | }, 22 | "Statistic": { 23 | "Type": 1 24 | }, 25 | "Comment": { 26 | "Type": 1 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/wwwroot/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redirecting... 7 | 8 | 9 | 20 | 21 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.Extensions.Logging; 4 | using System.Diagnostics; 5 | 6 | namespace AcBlog.Server.Api.Pages 7 | { 8 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 9 | public class ErrorModel : PageModel 10 | { 11 | public string RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Commands/CleanCommand.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Tools.Sdk.Models; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace AcBlog.Tools.Sdk.Commands 8 | { 9 | public class CleanCommand : BaseCommand 10 | { 11 | public override string Name => "clean"; 12 | 13 | public override string Description => "Clean temp files."; 14 | 15 | public override async Task Handle(CArgument argument, IHost host, CancellationToken cancellationToken) 16 | { 17 | Workspace workspace = host.Services.GetRequiredService(); 18 | await workspace.Clean(); 19 | return 0; 20 | } 21 | 22 | public class CArgument 23 | { 24 | 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/Extensions/DocumentExtensions.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Documents; 2 | using Markdig; 3 | using System; 4 | 5 | namespace AcBlog.Data.Extensions 6 | { 7 | public static class DocumentExtensions 8 | { 9 | public static int CountWords(this Document document) 10 | { 11 | return document.Raw.Length; 12 | } 13 | 14 | public static TimeSpan GetReadTime(this Document document) 15 | { 16 | const int WordPerMinute = 500; 17 | return TimeSpan.FromMinutes(document.CountWords() / (double)WordPerMinute); 18 | } 19 | 20 | public static string GetPreview(this Document document, int length = 300) 21 | { 22 | string pre = Markdown.ToPlainText(document.Raw); 23 | return pre.Length <= length ? pre : pre.Substring(0, length); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Commands/InitCommand.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Tools.Sdk.Models; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace AcBlog.Tools.Sdk.Commands 8 | { 9 | public class InitCommand : BaseCommand 10 | { 11 | public override string Name => "init"; 12 | 13 | public override string Description => "Initialize AcBlog."; 14 | 15 | public override async Task Handle(CArgument argument, IHost host, CancellationToken cancellationToken) 16 | { 17 | Workspace workspace = host.Services.GetRequiredService(); 18 | await workspace.Initialize(); 19 | return 0; 20 | } 21 | 22 | public class CArgument 23 | { 24 | 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Helpers/Equality.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace AcBlog.Tools.Sdk.Helpers 5 | { 6 | 7 | internal class Equality 8 | { 9 | [DoesNotReturn] 10 | void ThrowNeq() 11 | { 12 | throw new Exception(); 13 | } 14 | 15 | public void Add(T a, T b) 16 | { 17 | if (a is null && b is null) 18 | { 19 | return; 20 | } 21 | if (a is null || b is null) 22 | ThrowNeq(); 23 | if (!a.Equals(b)) 24 | ThrowNeq(); 25 | } 26 | 27 | public void Add(T[] a, T[] b) 28 | { 29 | if (a.Length != b.Length) 30 | ThrowNeq(); 31 | for (int i = 0; i < a.Length; i++) 32 | Add(a[i], b[i]); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/ServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.Models; 2 | using AcBlog.Client.UI.Models; 3 | using AcBlog.Extensions; 4 | using AcBlog.UI.Components; 5 | using AcBlog.UI.Components.Loading; 6 | using AcBlog.UI.Components.Slides; 7 | using Microsoft.AspNetCore.Components.Authorization; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace AcBlog.Client.UI 11 | { 12 | public static class ServiceExtensions 13 | { 14 | public static void AddUIComponents(this IServiceCollection services) 15 | { 16 | services.AddExtensions() 17 | .AddExtension() 18 | .AddExtension() 19 | .AddExtension() 20 | .AddExtension() 21 | .AddExtension(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly.Host/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace AcBlog.Client.WebAssembly.Host 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args) 16 | .ConfigureAppConfiguration((host, config) => 17 | { 18 | config.AddJsonFile("build.json"); 19 | }) 20 | .ConfigureWebHostDefaults(webBuilder => 21 | { 22 | webBuilder.UseStartup(); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "BaseAddress": "https://localhost:8201", 11 | "Server": { 12 | "Identity": { 13 | "Enable": true, 14 | "Endpoint": "https://localhost:8001/_configuration/AcBlog.Client.Server", 15 | "RemoteRegisterPath": "https://localhost:8001/Identity/Account/Register", 16 | "RemoteProfilePath": "https://localhost:8001/Identity/Account/Manage" 17 | }, 18 | "Main": { 19 | "Uri": "https://localhost:8001", 20 | "IsStatic": false 21 | }, 22 | "Statistic": { 23 | "Type": 1 24 | }, 25 | "Comment": { 26 | "Type": 1 27 | } 28 | }, 29 | "Blog": { 30 | "Onwer": "StardustDL" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Filters/PostKeywordFilter.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data; 2 | using AcBlog.Data.Extensions; 3 | using AcBlog.Data.Models; 4 | using AcBlog.Data.Models.Actions; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace AcBlog.Services.Filters 9 | { 10 | public class PostKeywordFilter : BaseQueryFilter 11 | { 12 | public PostKeywordFilter(IPostService baseService) : base(baseService) 13 | { 14 | } 15 | 16 | public override Task> Filter(Keyword? arg, Pagination? pagination = null, QueryTimeOrder order = QueryTimeOrder.None) 17 | { 18 | return BaseService.QueryPaging(new PostQueryRequest 19 | { 20 | Keywords = arg, 21 | Pagination = pagination, 22 | Order = order, 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.FileSystem/Readers/FileFSReader.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories.Searchers.Local; 4 | using StardustDL.Extensions.FileProviders; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Data.Repositories.FileSystem.Readers 10 | { 11 | public class FileFSReader : RecordFSReaderBase, IFileRepository 12 | { 13 | public FileFSReader(string rootPath, IFileProvider fileProvider) : base(rootPath, fileProvider) 14 | { 15 | } 16 | 17 | protected override IAsyncEnumerable? FullQuery(FileQueryRequest query, CancellationToken cancellationToken = default) 18 | { 19 | return new LocalFileRepositorySearcher().Search(this, query, cancellationToken); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Filters/PostCategoryFilter.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data; 2 | using AcBlog.Data.Extensions; 3 | using AcBlog.Data.Models; 4 | using AcBlog.Data.Models.Actions; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace AcBlog.Services.Filters 9 | { 10 | public class PostCategoryFilter : BaseQueryFilter 11 | { 12 | public PostCategoryFilter(IPostService baseService) : base(baseService) 13 | { 14 | } 15 | 16 | public override Task> Filter(Category? arg, Pagination? pagination = null, QueryTimeOrder order = QueryTimeOrder.None) 17 | { 18 | return BaseService.QueryPaging(new PostQueryRequest 19 | { 20 | Category = arg, 21 | Pagination = pagination, 22 | Order = order, 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Filters/PageRouteFilter.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data; 2 | using AcBlog.Data.Extensions; 3 | using AcBlog.Data.Models.Actions; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | 7 | namespace AcBlog.Services.Filters 8 | { 9 | public class PageRouteFilter : BaseQueryFilter 10 | { 11 | public PageRouteFilter(IPageService baseService) : base(baseService) 12 | { 13 | } 14 | 15 | public override Task> Filter(string? arg, Pagination? pagination = null, QueryTimeOrder order = QueryTimeOrder.None) 16 | { 17 | string route = (arg ?? string.Empty); 18 | return BaseService.QueryPaging(new PageQueryRequest 19 | { 20 | Route = route, 21 | Pagination = pagination, 22 | Order = order, 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.FileSystem/Readers/LayoutFSReader.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories.Searchers.Local; 4 | using StardustDL.Extensions.FileProviders; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Data.Repositories.FileSystem.Readers 10 | { 11 | public class LayoutFSReader : RecordFSReaderBase, ILayoutRepository 12 | { 13 | public LayoutFSReader(string rootPath, IFileProvider fileProvider) : base(rootPath, fileProvider) 14 | { 15 | } 16 | 17 | protected override IAsyncEnumerable? FullQuery(LayoutQueryRequest query, CancellationToken cancellationToken = default) 18 | { 19 | return new LocalLayoutRepositorySearcher().Search(this, query, cancellationToken); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Extensions/CommentServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using AcBlog.Data.Repositories; 5 | using AcBlog.Data.Repositories.Searchers; 6 | using AcBlog.Services; 7 | 8 | namespace AcBlog.Services.Extensions 9 | { 10 | public static class CommentServiceExtensions 11 | { 12 | public static ICommentService AsService(this ICommentRepository repository, IBlogService blogService) 13 | { 14 | return new RepoBasedService(blogService, repository); 15 | } 16 | 17 | class RepoBasedService : RecordRepoBasedService, ICommentService 18 | { 19 | public RepoBasedService(IBlogService blogService, ICommentRepository repository) : base(blogService, repository) 20 | { 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly.Host/Controllers/SiteController.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Sdk; 2 | using AcBlog.Services; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Configuration; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Xml; 8 | 9 | namespace AcBlog.Client.WebAssembly.Host.Controllers 10 | { 11 | [Route("[controller]")] 12 | [ApiController] 13 | public class SiteController : ControllerBase 14 | { 15 | 16 | private IBlogService BlogService { get; } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | private string BaseAddress { get; } 21 | 22 | public SiteController(IBlogService blogService, IConfiguration configuration) 23 | { 24 | BlogService = blogService; 25 | Configuration = configuration; 26 | BaseAddress = Configuration.GetBaseAddress().TrimEnd('/'); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/AcBlog.Services/Extensions/StatisticServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using AcBlog.Data.Repositories; 5 | using AcBlog.Data.Repositories.Searchers; 6 | using AcBlog.Services; 7 | 8 | namespace AcBlog.Services.Extensions 9 | { 10 | public static class StatisticServiceExtensions 11 | { 12 | public static IStatisticService AsService(this IStatisticRepository repository, IBlogService blogService) 13 | { 14 | return new RepoBasedService(blogService, repository); 15 | } 16 | 17 | class RepoBasedService : RecordRepoBasedService, IStatisticService 18 | { 19 | public RepoBasedService(IBlogService blogService, IStatisticRepository repository) : base(blogService, repository) 20 | { 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/Test.Data/Models/Actions/PaginationTest.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models.Actions; 2 | using DeepEqual.Syntax; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace Test.Data.Models.Actions 6 | { 7 | [TestClass] 8 | public class PaginationTest 9 | { 10 | [TestMethod] 11 | public void Basic() 12 | { 13 | Pagination page = new Pagination 14 | { 15 | CurrentPage = 0, 16 | PageSize = 10, 17 | TotalCount = 100 18 | }; 19 | Assert.AreEqual(10, page.TotalPage); 20 | Assert.IsTrue(page.HasNextPage); 21 | Assert.IsFalse(page.HasPreviousPage); 22 | 23 | page.NextPage().ShouldDeepEqual(new Pagination 24 | { 25 | CurrentPage = 1, 26 | PageSize = 10, 27 | TotalCount = 100 28 | }); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Controllers/PostsController.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Services; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Threading.Tasks; 6 | 7 | namespace AcBlog.Server.Api.Controllers 8 | { 9 | [Route("[controller]")] 10 | [ApiController] 11 | public class PostsController : RecordControllerBase 12 | { 13 | public PostsController(IBlogService service) : base(service.PostService) 14 | { 15 | 16 | } 17 | 18 | [HttpGet("categories")] 19 | public async Task> GetCategories() 20 | { 21 | return await Service.GetCategories(); 22 | } 23 | 24 | [HttpGet("keywords")] 25 | public async Task> GetKeywords() 26 | { 27 | return await Service.GetKeywords(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Helpers/ConsoleExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace AcBlog.Tools.Sdk.Helpers 5 | { 6 | 7 | static class ConsoleExtensions 8 | { 9 | public static string Input(string prompt = "") 10 | { 11 | Console.Write(prompt); 12 | return Console.ReadLine() ?? string.Empty; 13 | } 14 | 15 | public static string InputPassword(string prompt = "") 16 | { 17 | Console.Write(prompt); 18 | StringBuilder sb = new StringBuilder(); 19 | while (true) 20 | { 21 | var key = Console.ReadKey(true); 22 | if (key.Key is ConsoleKey.Enter) 23 | { 24 | Console.WriteLine(); 25 | break; 26 | } 27 | sb.Append(key.KeyChar); 28 | } 29 | return sb.ToString(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Helpers/FSExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AcBlog.Tools.Sdk.Helpers 4 | { 5 | internal static class FSExtensions 6 | { 7 | public static void CopyDirectory(string sourceDirPath, string saveDirPath) 8 | { 9 | if (!Directory.Exists(saveDirPath)) 10 | { 11 | Directory.CreateDirectory(saveDirPath); 12 | } 13 | string[] files = Directory.GetFiles(sourceDirPath); 14 | 15 | foreach (string file in files) 16 | { 17 | string pFilePath = Path.Join(saveDirPath, Path.GetFileName(file)); 18 | File.Copy(file, pFilePath, true); 19 | } 20 | 21 | string[] dirs = Directory.GetDirectories(sourceDirPath); 22 | foreach (string dir in dirs) 23 | { 24 | CopyDirectory(dir, Path.Join(saveDirPath, Path.GetFileName(dir))); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Pages/_Host.cshtml.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Sdk; 3 | using AcBlog.Services; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | using Microsoft.Extensions.Configuration; 6 | using System.Threading.Tasks; 7 | 8 | namespace AcBlog.Client.Server.Pages 9 | { 10 | public class HostModel : PageModel 11 | { 12 | public HostModel(IBlogService service, IConfiguration configuration) 13 | { 14 | Service = service; 15 | Configuration = configuration; 16 | BaseAddress = Configuration.GetBaseAddress().TrimEnd('/'); 17 | } 18 | 19 | public IBlogService Service { get; } 20 | public IConfiguration Configuration { get; } 21 | public string BaseAddress { get; } 22 | public BlogOptions BlogOptions { get; set; } 23 | 24 | public async Task OnGetAsync() 25 | { 26 | BlogOptions = await Service.GetOptions(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly.Host/AcBlog.Client.WebAssembly.Host.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 399247a7-75aa-4e46-9ca1-375446e64efb 6 | Linux 7 | ..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly.Host/Controllers/ServerController.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Client.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace AcBlog.Client.WebAssembly.Host.Controllers 6 | { 7 | [Route("[controller]")] 8 | [ApiController] 9 | public class ServerController : ControllerBase 10 | { 11 | public ServerController(IOptions serverSettings, IOptions buildStatus) 12 | { 13 | ServerSettings = serverSettings.Value; 14 | BuildStatus = buildStatus.Value; 15 | } 16 | 17 | public ServerSettings ServerSettings { get; } 18 | 19 | public BuildStatus BuildStatus { get; } 20 | 21 | [HttpGet("Test")] 22 | public bool Test() => true; 23 | 24 | [HttpGet("Server")] 25 | public object Server() => new { Server = ServerSettings }; 26 | 27 | [HttpGet("Build")] 28 | public object Build() => new { Build = BuildStatus }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Pages/OpenSearchDescription.cshtml.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Sdk; 3 | using AcBlog.Services; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | using Microsoft.Extensions.Configuration; 6 | using System.Threading.Tasks; 7 | 8 | namespace AcBlog.Client.Server.Pages 9 | { 10 | public class OpenSearchDescriptionModel : PageModel 11 | { 12 | public OpenSearchDescriptionModel(IBlogService service, IConfiguration configuration) 13 | { 14 | Service = service; 15 | Configuration = configuration; 16 | BaseAddress = Configuration.GetBaseAddress().TrimEnd('/'); 17 | } 18 | 19 | public IBlogService Service { get; } 20 | public IConfiguration Configuration { get; } 21 | public string BaseAddress { get; } 22 | public BlogOptions BlogOptions { get; set; } 23 | 24 | public async Task OnGetAsync() 25 | { 26 | BlogOptions = await Service.GetOptions(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AcBlog.Server.Api.Pages.ErrorModel 3 | @{ 4 | Layout = "_Layout"; 5 | ViewData["Title"] = "Error"; 6 | } 7 | 8 |

Error.

9 |

An error occurred while processing your request.

10 | 11 | @if (Model.ShowRequestId) 12 | { 13 |

14 | Request ID: @Model.RequestId 15 |

16 | } 17 | 18 |

Development Mode

19 |

20 | Swapping to the Development environment displays detailed information about the error that occurred. 21 |

22 |

23 | The Development environment shouldn't be enabled for deployed applications. 24 | It can result in displaying sensitive information from exceptions to end users. 25 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 26 | and restarting the app. 27 |

28 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:62345", 7 | "sslPort": 44306 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "AcBlog.Client.Server": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:8201;http://localhost:8200" 25 | }, 26 | "Docker": { 27 | "commandName": "Docker", 28 | "launchBrowser": true, 29 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 30 | "publishAllPorts": true, 31 | "useSSL": true 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Utils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace AcBlog.Client.Server 7 | { 8 | public static class Utils 9 | { 10 | public static async Task WaitAsync(this Task task, TimeSpan timeout) 11 | { 12 | using var timeoutCancellationTokenSource = new CancellationTokenSource(); 13 | var delayTask = Task.Delay(timeout, timeoutCancellationTokenSource.Token); 14 | if (await Task.WhenAny(task, delayTask) == task) 15 | { 16 | timeoutCancellationTokenSource.Cancel(); 17 | return await task; 18 | } 19 | throw new TimeoutException("The operation has timed out."); 20 | } 21 | 22 | public static string GetBaseAddress(this IConfiguration configuration) 23 | { 24 | return configuration.GetValue("BaseAddress"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly.Host/Utils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace AcBlog.Client.WebAssembly.Host 7 | { 8 | public static class Utils 9 | { 10 | public static async Task WaitAsync(this Task task, TimeSpan timeout) 11 | { 12 | using var timeoutCancellationTokenSource = new CancellationTokenSource(); 13 | var delayTask = Task.Delay(timeout, timeoutCancellationTokenSource.Token); 14 | if (await Task.WhenAny(task, delayTask) == task) 15 | { 16 | timeoutCancellationTokenSource.Cancel(); 17 | return await task; 18 | } 19 | throw new TimeoutException("The operation has timed out."); 20 | } 21 | 22 | public static string GetBaseAddress(this IConfiguration configuration) 23 | { 24 | return configuration.GetValue("BaseAddress"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:52733", 7 | "sslPort": 44330 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | }, 17 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 18 | }, 19 | "AcBlog.Client.WebAssembly": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "applicationUrl": "https://localhost:8151;http://localhost:8150", 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/AcBlog.Data/AcBlog.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | enable 6 | true 7 | 8 | 9 | StardustDL 10 | AcBlog 11 | Apache-2.0 12 | https://github.com/acblog 13 | https://github.com/acblog/acblog 14 | Git 15 | icon.png 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | True 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Components/Displays/PostCommentDisplay.razor: -------------------------------------------------------------------------------- 1 | @inject IBlogService Service 2 | @inject IClientUriGenerator UrlGenerator 3 | 4 | @if (Count is not null) 5 | { 6 | 7 | @Count.Value 8 | 9 | } 10 | 11 | @code { 12 | [Parameter] 13 | public Post Data { get; set; } 14 | 15 | private long? Count { get; set; } 16 | 17 | protected override async Task OnParametersSetAsync() 18 | { 19 | try 20 | { 21 | var query = await Service.CommentService.QueryPaging(new CommentQueryRequest 22 | { 23 | Uri = UrlGenerator.Post(Data), 24 | Pagination = new Data.Models.Actions.Pagination 25 | { 26 | CurrentPage = 0, 27 | PageSize = 1 28 | } 29 | }); 30 | Count = query.CurrentPage.TotalCount; 31 | } 32 | catch 33 | { 34 | Count = null; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Extensions/PostServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Services.Filters; 2 | 3 | namespace AcBlog.Services.Extensions 4 | { 5 | public static class PostServiceExtensions 6 | { 7 | public static PostArticleFilter CreateArticleFilter(this IPostService service) 8 | { 9 | return new PostArticleFilter(service); 10 | } 11 | 12 | public static PostSlidesFilter CreateSlidesFilter(this IPostService service) 13 | { 14 | return new PostSlidesFilter(service); 15 | } 16 | 17 | public static PostNoteFilter CreateNoteFilter(this IPostService service) 18 | { 19 | return new PostNoteFilter(service); 20 | } 21 | 22 | public static PostKeywordFilter CreateKeywordFilter(this IPostService service) 23 | { 24 | return new PostKeywordFilter(service); 25 | } 26 | 27 | public static PostCategoryFilter CreateCategoryFilter(this IPostService service) 28 | { 29 | return new PostCategoryFilter(service); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/Repositories/Searchers/Local/LocalStatisticRepositorySearcher.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Data.Repositories.Searchers.Local 10 | { 11 | public class LocalStatisticRepositorySearcher : IStatisticRepositorySearcher 12 | { 13 | public IAsyncEnumerable Search(IStatisticRepository repository, StatisticQueryRequest query, CancellationToken cancellationToken = default) 14 | { 15 | var qr = repository.GetAllItems(cancellationToken).IgnoreNull(); 16 | 17 | if (!string.IsNullOrWhiteSpace(query.Category)) 18 | qr = qr.Where(x => x.Category == query.Category); 19 | if (!string.IsNullOrWhiteSpace(query.Uri)) 20 | qr = qr.Where(x => x.Uri == query.Uri); 21 | 22 | return qr.Select(item => item.Id).IgnoreNull().Paging(query.Pagination); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/ClientUIComponent.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Services; 2 | using AcBlog.UI.Components; 3 | using Microsoft.AspNetCore.Components.Authorization; 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | 7 | namespace AcBlog.Client.UI 8 | { 9 | 10 | 11 | public class ClientUIComponent : UIComponent 12 | { 13 | public ClientUIComponent() 14 | { 15 | AddLocalStyleSheetResource("lib/bootstrap/css/bootstrap.min.css"); 16 | AddStyleSheetResource("_content/StardustDL.RazorComponents.MaterialDesignIcons/mdi/css/materialdesignicons.min.css"); 17 | AddLocalStyleSheetResource("lib/markdown-css/github-markdown.css"); 18 | AddLocalStyleSheetResource("css/app.css"); 19 | 20 | AddLocalScriptResource("js/interop.js"); 21 | AddScriptResource("_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"); 22 | AddLocalScriptResource("lib/jquery/jquery.slim.min.js"); 23 | // AddScriptResource("lib/bootstrap/js/bootstrap.bundle.min.js"); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly.Host/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:58374", 7 | "sslPort": 44376 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "AcBlog.Client.WebAssembly.Host": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "https://localhost:8101", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "applicationUrl": "https://localhost:8101;http://localhost:8100" 26 | }, 27 | "Docker": { 28 | "commandName": "Docker", 29 | "launchBrowser": true, 30 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 31 | "publishAllPorts": true, 32 | "useSSL": true 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.SqlServer/Models/RawUser.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | 3 | namespace AcBlog.Data.Repositories.SqlServer.Models 4 | { 5 | public class RawUser : IHasId 6 | { 7 | public string Id { get; set; } = string.Empty; 8 | 9 | public string Name { get; set; } = string.Empty; 10 | 11 | public string NickName { get; set; } = string.Empty; 12 | 13 | public string Email { get; set; } = string.Empty; 14 | 15 | public static RawUser From(User value) 16 | { 17 | return new RawUser 18 | { 19 | Id = value.Id, 20 | NickName = value.NickName, 21 | Name = value.Name, 22 | Email = value.Email, 23 | }; 24 | } 25 | 26 | public static User To(RawUser value) 27 | { 28 | return new User 29 | { 30 | Id = value.Id, 31 | Name = value.Name, 32 | NickName = value.NickName, 33 | Email = value.Email, 34 | }; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Controllers/OidcConfigurationController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace AcBlog.Server.Api.Controllers 6 | { 7 | public class OidcConfigurationController : Controller 8 | { 9 | private readonly ILogger _logger; 10 | 11 | public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider, ILogger logger) 12 | { 13 | ClientRequestParametersProvider = clientRequestParametersProvider; 14 | _logger = logger; 15 | } 16 | 17 | public IClientRequestParametersProvider ClientRequestParametersProvider { get; } 18 | 19 | [HttpGet("_configuration/{clientId}")] 20 | public IActionResult GetClientRequestParameters([FromRoute] string clientId) 21 | { 22 | var parameters = ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId); 23 | return Ok(parameters); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AcBlog.Services/IBlogService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Services.Models; 5 | using System; 6 | using System.Text.Json; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace AcBlog.Services 11 | { 12 | public interface IBlogService 13 | { 14 | IPostService PostService { get; } 15 | 16 | IPageService PageService { get; } 17 | 18 | ILayoutService LayoutService { get; } 19 | 20 | IUserService UserService { get; } 21 | 22 | ICommentService CommentService { get; } 23 | 24 | IStatisticService StatisticService { get; } 25 | 26 | IFileService FileService { get; } 27 | 28 | RepositoryAccessContext Context { get; } 29 | 30 | Task GetOptions(CancellationToken cancellationToken = default); 31 | 32 | Task SetOptions(BlogOptions options, CancellationToken cancellationToken = default); 33 | 34 | Task> Query(BlogQueryRequest query, CancellationToken cancellationToken = default); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Test.Utils/Data/Protections/ProtectorTester.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Protections; 2 | using DeepEqual.Syntax; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System.Threading.Tasks; 5 | 6 | namespace Test.Data.Protections 7 | { 8 | public static class ProtectorTester 9 | { 10 | public static async Task TestPassword(this IProtector protector, T raw) 11 | { 12 | ProtectionKey key = new ProtectionKey 13 | { 14 | Password = "123" 15 | }; 16 | var pro = await protector.Protect(raw, key); 17 | 18 | Assert.IsFalse(await protector.IsProtected(raw)); 19 | Assert.IsTrue(await protector.IsProtected(pro)); 20 | 21 | var depro = await protector.Deprotect(pro, key); 22 | Assert.IsFalse(await protector.IsProtected(depro)); 23 | depro.ShouldDeepEqual(raw); 24 | 25 | /*await Assert.ThrowsExceptionAsync(() => protector.Deprotect(pro, new ProtectionKey 26 | { 27 | Password = "abc" 28 | }));*/ 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/AcBlog.Services.SqlServer/UserService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Data.Repositories.SqlServer; 5 | using AcBlog.Data.Repositories.SqlServer.Models; 6 | using AcBlog.Services.Models; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace AcBlog.Services.SqlServer 11 | { 12 | internal class UserService : RecordRepoBasedService, IUserService 13 | { 14 | public UserService(IBlogService blog, BlogDataContext dataContext) : base(blog, new UserDBRepository(dataContext)) 15 | { 16 | } 17 | 18 | public Task ChangePassword(UserChangePasswordRequest request, CancellationToken cancellationToken = default) => throw new System.NotImplementedException(); 19 | public Task GetCurrent(CancellationToken cancellationToken = default) => throw new System.NotImplementedException(); 20 | public Task Login(UserLoginRequest request, CancellationToken cancellationToken = default) => throw new System.NotImplementedException(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/Test.Data/Models/Builders/FeatureBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Builders; 3 | using DeepEqual.Syntax; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System; 6 | 7 | namespace Test.Data.Models.Builders 8 | { 9 | [TestClass] 10 | public class FeatureBuilderTest 11 | { 12 | [TestMethod] 13 | public void Basic() 14 | { 15 | FeatureBuilder builder = new FeatureBuilder(); 16 | builder.AddFeature("a", "b"); 17 | builder.Build().ShouldDeepEqual(new Feature{ Items = new[] { "a", "b" }}); 18 | 19 | builder.AddFeature("c"); 20 | builder.Build().ShouldDeepEqual(new Feature{ Items = new[] { "a", "b", "c" }}); 21 | 22 | builder.RemoveFeature("c"); 23 | builder.Build().ShouldDeepEqual(new Feature{ Items = new[] { "a", "b" }}); 24 | 25 | Assert.ThrowsException(() => builder.AddFeature($"a,b")); 26 | } 27 | 28 | [TestMethod] 29 | public void String() 30 | { 31 | _ = Generator.GetFeature().ToString(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Filters/BaseQueryFilter.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data; 2 | using AcBlog.Data.Models.Actions; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace AcBlog.Services.Filters 7 | { 8 | public abstract class BaseQueryFilter : IQueryFilter 9 | { 10 | protected BaseQueryFilter(TService baseService) 11 | { 12 | BaseService = baseService; 13 | } 14 | 15 | public TService BaseService { get; protected set; } 16 | 17 | public abstract Task> Filter(Pagination? pagination = null, QueryTimeOrder order = QueryTimeOrder.None); 18 | } 19 | 20 | public abstract class BaseQueryFilter : IQueryFilter 21 | { 22 | protected BaseQueryFilter(TService baseService) 23 | { 24 | BaseService = baseService; 25 | } 26 | 27 | public TService BaseService { get; protected set; } 28 | 29 | public abstract Task> Filter(T arg, Pagination? pagination = null, QueryTimeOrder order = QueryTimeOrder.None); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/Test.Data/Models/Builders/KeywordBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Builders; 3 | using DeepEqual.Syntax; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System; 6 | 7 | namespace Test.Data.Models.Builders 8 | { 9 | [TestClass] 10 | public class KeywordBuilderTest 11 | { 12 | [TestMethod] 13 | public void Basic() 14 | { 15 | KeywordBuilder builder = new KeywordBuilder(); 16 | builder.AddKeyword("a", "b"); 17 | builder.Build().ShouldDeepEqual(new Keyword{ Items = new[] { "a", "b" }}); 18 | 19 | builder.AddKeyword("c"); 20 | builder.Build().ShouldDeepEqual(new Keyword{ Items = new[] { "a", "b", "c" }}); 21 | 22 | builder.RemoveKeyword("c"); 23 | builder.Build().ShouldDeepEqual(new Keyword{ Items = new[] { "a", "b" }}); 24 | 25 | Assert.ThrowsException(() => builder.AddKeyword($"a;b")); 26 | } 27 | 28 | [TestMethod] 29 | public void String() 30 | { 31 | _ = Generator.GetKeyword().ToString(); 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:56049", 7 | "sslPort": 44370 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "AcBlog.Server.Api": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "https://localhost:8001;http://localhost:8000" 28 | }, 29 | "Docker": { 30 | "commandName": "Docker", 31 | "launchBrowser": true, 32 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", 33 | "publishAllPorts": true, 34 | "useSSL": true 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/AcBlog.Services.SqlServer/PostService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Documents; 2 | using AcBlog.Data.Extensions; 3 | using AcBlog.Data.Models; 4 | using AcBlog.Data.Models.Actions; 5 | using AcBlog.Data.Protections; 6 | using AcBlog.Data.Repositories; 7 | using AcBlog.Data.Repositories.SqlServer; 8 | using AcBlog.Data.Repositories.SqlServer.Models; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace AcBlog.Services.SqlServer 13 | { 14 | internal class PostService : RecordRepoBasedService, IPostService 15 | { 16 | public PostService(IBlogService blog, BlogDataContext dataContext) : base(blog, new PostDBRepository(dataContext)) 17 | { 18 | Protector = new DocumentProtector(); 19 | } 20 | 21 | public IProtector Protector { get; } 22 | 23 | public Task GetCategories(CancellationToken cancellationToken = default) => Repository.GetCategories(cancellationToken); 24 | 25 | public Task GetKeywords(CancellationToken cancellationToken = default) => Repository.GetKeywords(cancellationToken); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/Test.Data/Models/Builders/CategoryBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Builders; 3 | using DeepEqual.Syntax; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System; 6 | 7 | namespace Test.Data.Models.Builders 8 | { 9 | [TestClass] 10 | public class CategoryBuilderTest 11 | { 12 | [TestMethod] 13 | public void Basic() 14 | { 15 | CategoryBuilder builder = new CategoryBuilder(); 16 | builder.AddSubCategory("a", "b"); 17 | builder.Build().ShouldDeepEqual(new Category { Items = new[] { "a", "b" } }); 18 | 19 | builder.AddSubCategory("c"); 20 | builder.Build().ShouldDeepEqual(new Category { Items = new[] { "a", "b", "c" } }); 21 | 22 | builder.RemoveSubCategory(); 23 | builder.Build().ShouldDeepEqual(new Category { Items = new[] { "a", "b" } }); 24 | 25 | Assert.ThrowsException(() => builder.AddSubCategory($"a/b")); 26 | } 27 | 28 | [TestMethod] 29 | public void String() 30 | { 31 | _ = Generator.GetCategory().ToString(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/Test.Data/Test.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 9 | WORKDIR /src 10 | COPY ["src/AcBlog.Server.Api/AcBlog.Server.Api.csproj", "src/AcBlog.Server.Api/"] 11 | COPY ["src/AcBlog.Data/AcBlog.Data.csproj", "src/AcBlog.Data/"] 12 | COPY ["src/AcBlog.Data.Repositories.SqlServer/AcBlog.Data.Repositories.SqlServer.csproj", "src/AcBlog.Data.Repositories.SqlServer/"] 13 | RUN dotnet restore "src/AcBlog.Server.Api/AcBlog.Server.Api.csproj" -s https://sparkshine.pkgs.visualstudio.com/StardustDL/_packaging/feed/nuget/v3/index.json -s https://api.nuget.org/v3/index.json 14 | COPY . . 15 | WORKDIR "/src/src/AcBlog.Server.Api" 16 | RUN dotnet build "AcBlog.Server.Api.csproj" -c Release -o /app/build 17 | 18 | FROM build AS publish 19 | RUN dotnet publish "AcBlog.Server.Api.csproj" -c Release -o /app/publish 20 | 21 | FROM base AS final 22 | WORKDIR /app 23 | COPY --from=publish /app/publish . 24 | ENTRYPOINT ["dotnet", "AcBlog.Server.Api.dll"] 25 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Pages/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using AcBlog.Server.Api.Models 3 | 4 | @inject SignInManager SignInManager 5 | @inject UserManager UserManager 6 | 7 | 29 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/Extensions/PagingDataExtensions.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models.Actions; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace AcBlog.Data.Extensions 7 | { 8 | public static class PagingDataExtensions 9 | { 10 | public static PagingData AsPagingData(this IEnumerable data, QueryRequest query, QueryStatistic statistic) 11 | { 12 | Pagination pagination; 13 | 14 | if (query.Pagination is not null) 15 | { 16 | pagination = query.Pagination with { TotalCount = statistic.Count }; 17 | } 18 | else 19 | { 20 | pagination = new Pagination { CurrentPage = 0, PageSize = statistic.Count, TotalCount = statistic.Count }; 21 | } 22 | 23 | return new PagingData(data, pagination); 24 | } 25 | 26 | public static async Task> AsPagingData(this IAsyncEnumerable data, QueryRequest query, QueryStatistic statistic) 27 | { 28 | return (await data.ToArrayAsync().ConfigureAwait(false)).AsPagingData(query, statistic); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/StardustDL.Extensions.FileProviders.Http/HttpFileInfo.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace StardustDL.Extensions.FileProviders.Http 6 | { 7 | internal class HttpFileInfo : IFileInfo 8 | { 9 | private HttpResponseMessage Response { get; } 10 | 11 | public HttpFileInfo(HttpResponseMessage response) 12 | { 13 | Response = response; 14 | } 15 | 16 | public Task Exists() => Task.FromResult(Response.IsSuccessStatusCode); 17 | 18 | public Task Length() => Task.FromResult(Response.Content.Headers.ContentLength ?? 0); 19 | 20 | public string PhysicalPath => Response.Content.Headers.ContentLocation.AbsoluteUri; 21 | 22 | public string Name => Response.Content.Headers.ContentLocation.LocalPath; 23 | 24 | // public DateTimeOffset LastModified => Response.Content.Headers.LastModified ?? DateTimeOffset.MinValue; 25 | 26 | public bool IsDirectory => false; 27 | 28 | public async Task CreateReadStream() 29 | { 30 | return await Response.Content.ReadAsStreamAsync().ConfigureAwait(false); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.FileSystem/Builders/PageRepositoryBuilder.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | 7 | namespace AcBlog.Data.Repositories.FileSystem.Builders 8 | { 9 | public class PageRepositoryBuilder : RecordFSBuilderBase 10 | { 11 | public PageRepositoryBuilder(string rootPath) : base(rootPath) 12 | { 13 | } 14 | 15 | async Task BuildRouteIndex(IList data) 16 | { 17 | var gr = from x in data group x by x.Route; 18 | 19 | foreach (var g in gr) 20 | { 21 | string path = Paths.GetRouteFile(RootPath, g.Key); 22 | await using var st = FSStaticBuilder.GetFileRewriteStream(path); 23 | await JsonSerializer.SerializeAsync(st, (from p in g select p.Id).ToArray()).ConfigureAwait(false); 24 | } 25 | } 26 | 27 | public override async Task Build(IList data) 28 | { 29 | await base.Build(data).ConfigureAwait(false); 30 | await BuildRouteIndex(data).ConfigureAwait(false); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.SqlServer/Models/RawLayout.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using System; 3 | 4 | namespace AcBlog.Data.Repositories.SqlServer.Models 5 | { 6 | public class RawLayout : IHasId 7 | { 8 | public string Id { get; set; } = string.Empty; 9 | 10 | public string Template { get; set; } = string.Empty; 11 | 12 | public DateTimeOffset CreationTime { get; set; } 13 | 14 | public DateTimeOffset ModificationTime { get; set; } 15 | 16 | public static RawLayout From(Layout value) 17 | { 18 | return new RawLayout 19 | { 20 | Id = value.Id, 21 | Template = value.Template, 22 | CreationTime = value.CreationTime, 23 | ModificationTime = value.ModificationTime, 24 | }; 25 | } 26 | 27 | public static Layout To(RawLayout value) 28 | { 29 | return new Layout 30 | { 31 | Id = value.Id, 32 | Template = value.Template, 33 | CreationTime = value.CreationTime, 34 | ModificationTime = value.ModificationTime, 35 | }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/Repositories/Searchers/Local/LocalPageRepositorySearcher.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Extensions; 2 | using AcBlog.Data.Models; 3 | using AcBlog.Data.Models.Actions; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Data.Repositories.Searchers.Local 10 | { 11 | public class LocalPageRepositorySearcher : IPageRepositorySearcher 12 | { 13 | public IAsyncEnumerable Search(IPageRepository repository, PageQueryRequest query, CancellationToken cancellationToken = default) 14 | { 15 | var qr = repository.GetAllItems(cancellationToken).IgnoreNull(); 16 | if (string.IsNullOrEmpty(query.Route)) 17 | qr = qr.Where(x => x.Route.StartsWith(query.Route)); 18 | if (!string.IsNullOrWhiteSpace(query.Term)) 19 | { 20 | qr = qr.Where(x => 21 | x.Title.ToString().Contains(query.Term) || 22 | x.Content.ToString().Contains(query.Term) 23 | ); 24 | } 25 | return qr.Select(item => item.Id).IgnoreNull().Paging(query.Pagination); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Repositories/PostService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Documents; 2 | using AcBlog.Data.Extensions; 3 | using AcBlog.Data.Models; 4 | using AcBlog.Data.Models.Actions; 5 | using AcBlog.Data.Protections; 6 | using AcBlog.Data.Repositories; 7 | using AcBlog.Data.Repositories.Searchers; 8 | using AcBlog.Sdk; 9 | using AcBlog.Services; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace AcBlog.Tools.Sdk.Repositories 14 | { 15 | internal class PostService : RecordRepoBasedService, IPostService 16 | { 17 | public PostService(IBlogService blog, string rootPath) : base(blog, new PostFSRepo(rootPath, new DocumentProtector())) 18 | { 19 | } 20 | 21 | internal PostFSRepo InnerRepository => (Repository as PostFSRepo)!; 22 | 23 | public IProtector Protector => InnerRepository.Protector; 24 | 25 | public Task GetCategories(CancellationToken cancellationToken = default) => Repository.GetCategories(cancellationToken); 26 | 27 | public Task GetKeywords(CancellationToken cancellationToken = default) => Repository.GetKeywords(cancellationToken); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AcBlog.Services.FileSystem/PostService.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Documents; 2 | using AcBlog.Data.Extensions; 3 | using AcBlog.Data.Models; 4 | using AcBlog.Data.Models.Actions; 5 | using AcBlog.Data.Protections; 6 | using AcBlog.Data.Repositories; 7 | using AcBlog.Data.Repositories.FileSystem.Readers; 8 | using AcBlog.Data.Repositories.Searchers; 9 | using StardustDL.Extensions.FileProviders; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace AcBlog.Services.FileSystem 14 | { 15 | internal class PostService : RecordRepoBasedService, IPostService 16 | { 17 | public PostService(IBlogService blog, string rootPath, IFileProvider fileProvider) : base(blog, new PostFSReader(rootPath, fileProvider)) 18 | { 19 | Protector = new DocumentProtector(); 20 | } 21 | 22 | public IProtector Protector { get; } 23 | 24 | public Task GetCategories(CancellationToken cancellationToken = default) => Repository.GetCategories(cancellationToken); 25 | 26 | public Task GetKeywords(CancellationToken cancellationToken = default) => Repository.GetKeywords(cancellationToken); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.Externals/AcBlog.Data.Repositories.Externals.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | enable 6 | true 7 | 8 | 9 | StardustDL 10 | AcBlog 11 | Apache-2.0 12 | https://github.com/acblog 13 | https://github.com/acblog/acblog 14 | Git 15 | icon.png 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | True 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Authorization 8 | @using Microsoft.JSInterop 9 | @using Microsoft.Extensions.Logging 10 | @using Microsoft.Extensions.Options 11 | @using Microsoft.AspNetCore.Authorization 12 | @using Microsoft.AspNetCore.Components.Web.Virtualization 13 | @using Microsoft.AspNetCore.Components.Web.Extensions.Head 14 | @using AcBlog.Client.UI 15 | @using AcBlog.Client.UI.Shared 16 | @using AcBlog.Sdk 17 | @using AcBlog.Services.Generators 18 | @using AcBlog.Services.Extensions 19 | @using AcBlog.Data 20 | @using AcBlog.Data.Models 21 | @using AcBlog.Data.Extensions 22 | @using AcBlog.Data.Pages 23 | @using AcBlog.Data.Documents 24 | @using AcBlog.Data.Models.Actions 25 | @using AcBlog.Services 26 | @using AcBlog.Client 27 | @using AcBlog.Client.Models 28 | @using AcBlog.Client.UI.Models 29 | @using AcBlog.Client.UI.Components 30 | @using AcBlog.Client.UI.Interops 31 | @using AcBlog.Client.Helpers 32 | 33 | @using System.Web 34 | 35 | @using AntDesign -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Core/AccessTokenProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Components.Web.Extensions; 4 | using Microsoft.JSInterop; 5 | 6 | namespace AcBlog.Client 7 | { 8 | public class AccessTokenProvider 9 | { 10 | public AccessTokenProvider(IJSRuntime jSRuntime) => JSRuntime = jSRuntime; 11 | 12 | /*public AccessTokenProvider(ProtectedLocalStorage protectedLocalStorage) 13 | { 14 | ProtectedLocalStorage = protectedLocalStorage; 15 | } 16 | 17 | ProtectedLocalStorage ProtectedLocalStorage { get; }*/ 18 | 19 | IJSRuntime JSRuntime { get; } 20 | 21 | public event Action TokenChanged; 22 | 23 | public async Task GetToken() 24 | { 25 | try 26 | { 27 | return await JSRuntime.InvokeAsync("localStorage.getItem", "access_token"); 28 | } 29 | catch 30 | { 31 | return ""; 32 | } 33 | } 34 | 35 | public async Task SetToken(string token) 36 | { 37 | await JSRuntime.InvokeVoidAsync("localStorage.setItem", "access_token", token); 38 | 39 | TokenChanged?.Invoke(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.SqlServer/AcBlog.Data.Repositories.SqlServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | true 6 | 7 | 8 | StardustDL 9 | AcBlog 10 | Apache-2.0 11 | https://github.com/acblog 12 | https://github.com/acblog/acblog 13 | Git 14 | icon.png 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | True 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/wwwroot/css/mdi.css: -------------------------------------------------------------------------------- 1 | .alert.mdi::before, 2 | .breadcrumb .mdi::before, 3 | .btn.mdi::before, 4 | .card-title.mdi::before, 5 | .card-subtitle.mdi::before, 6 | .card-link.mdi::before, 7 | .dropdown-item.mdi::before, 8 | .list-group-item.mdi::before, 9 | .nav-link.mdi::before { 10 | font-size: 1.25em; 11 | line-height: initial; 12 | position: relative; 13 | top: 0.09rem; 14 | } 15 | 16 | .itemdisplay .mdi:not(:empty)::before { 17 | margin-right: 0.25rem; 18 | } 19 | 20 | .alert.mdi::before, 21 | .breadcrumb .mdi:not(:empty)::before, 22 | .btn.mdi:not(:empty)::before, 23 | .card-title.mdi:not(:empty)::before, 24 | .card-subtitle.mdi:not(:empty)::before, 25 | .card-link.mdi:not(:empty)::before, 26 | .dropdown-item.mdi:not(:empty)::before, 27 | .nav-link.mdi:not(:empty)::before { 28 | margin-right: 0.25rem; 29 | } 30 | 31 | .list-group-item.mdi:not(:empty)::before { 32 | margin-right: 0.5rem; 33 | } 34 | 35 | .dropdown-item.mdi:not(:empty)::before { 36 | margin-left: -0.75rem; 37 | } 38 | 39 | .alert.mdi::before, 40 | .list-group-item.mdi:not(:empty)::before { 41 | margin-left: -0.5rem; 42 | } 43 | 44 | .modal-title.mdi::before { 45 | font-size: 1.5em; 46 | line-height: 0.5; 47 | position: relative; 48 | top: 0.26rem; 49 | margin-right: 0.5rem; 50 | } 51 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Controllers/BlogController.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Services; 4 | using AcBlog.Services.Models; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Server.Api.Controllers 10 | { 11 | [Route("[controller]")] 12 | [ApiController] 13 | public class BlogController : ControllerBase 14 | { 15 | public BlogController(IBlogService blogService) 16 | { 17 | BlogService = blogService; 18 | } 19 | 20 | public IBlogService BlogService { get; } 21 | 22 | 23 | [HttpGet("options")] 24 | public async Task GetOptions() 25 | { 26 | return await BlogService.GetOptions(); 27 | } 28 | 29 | [HttpPost("query")] 30 | public async Task> Query([FromBody] BlogQueryRequest query) 31 | { 32 | return await BlogService.Query(query); 33 | } 34 | 35 | [HttpPost("options")] 36 | [Authorize] 37 | public async Task SetOptions([FromBody] BlogOptions options) 38 | { 39 | return await BlogService.SetOptions(options); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Pages/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Models/Text/LayoutMetadata.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using System; 3 | 4 | #pragma warning disable IDE1006 // 命名样式 5 | 6 | namespace AcBlog.Tools.Sdk.Models.Text 7 | { 8 | public class LayoutMetadata : MetadataBase 9 | { 10 | public string id { get; set; } = string.Empty; 11 | 12 | public string creationTime { get; set; } = string.Empty; 13 | 14 | public string modificationTime { get; set; } = string.Empty; 15 | 16 | public LayoutMetadata() { } 17 | 18 | public LayoutMetadata(Layout data) 19 | { 20 | id = data.Id; 21 | creationTime = data.CreationTime.ToString(); 22 | modificationTime = data.ModificationTime.ToString(); 23 | } 24 | 25 | public override Layout ApplyTo(Layout data) 26 | { 27 | data = data with { Id = id }; 28 | if (DateTimeOffset.TryParse(creationTime, out var _creationTime)) 29 | { 30 | data = data with { CreationTime = _creationTime }; 31 | } 32 | if (DateTimeOffset.TryParse(modificationTime, out var _modificationTime)) 33 | { 34 | data = data with { ModificationTime = _modificationTime }; 35 | } 36 | return data; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Extensions/AcBlog.Data.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | enable 6 | true 7 | 8 | 9 | StardustDL 10 | AcBlog 11 | Apache-2.0 12 | https://github.com/acblog 13 | https://github.com/acblog/acblog 14 | Git 15 | icon.png 16 | AcBlog.Data 17 | 18 | 19 | 20 | 21 | True 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Builders/FeatureBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace AcBlog.Data.Models.Builders 6 | { 7 | public class FeatureBuilder 8 | { 9 | HashSet Inner { get; set; } = new HashSet(); 10 | 11 | public FeatureBuilder AddFeature(params string[] names) 12 | { 13 | foreach (var name in names) 14 | { 15 | if (!name.Contains(',')) 16 | { 17 | Inner.Add(name); 18 | } 19 | else 20 | { 21 | throw new Exception($"Invalid feature name: {name}."); 22 | } 23 | } 24 | return this; 25 | } 26 | 27 | public FeatureBuilder RemoveFeature(params string[] names) 28 | { 29 | foreach (var name in names) 30 | Inner.Remove(name); 31 | return this; 32 | } 33 | 34 | public bool IsEmpty => Inner.Count > 0; 35 | 36 | public Feature Build() => new Feature { Items = Inner.ToArray() }; 37 | 38 | public static Feature FromString(string source) 39 | { 40 | return new Feature { Items = source.Split(',', StringSplitOptions.RemoveEmptyEntries) }; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Builders/KeywordBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace AcBlog.Data.Models.Builders 6 | { 7 | public class KeywordBuilder 8 | { 9 | HashSet Inner { get; set; } = new HashSet(); 10 | 11 | public KeywordBuilder AddKeyword(params string[] names) 12 | { 13 | foreach (var name in names) 14 | { 15 | if (!name.Contains(';')) 16 | { 17 | Inner.Add(name); 18 | } 19 | else 20 | { 21 | throw new Exception($"Invalid keyword name: {name}."); 22 | } 23 | } 24 | return this; 25 | } 26 | 27 | public KeywordBuilder RemoveKeyword(params string[] names) 28 | { 29 | foreach (var name in names) 30 | Inner.Remove(name); 31 | return this; 32 | } 33 | 34 | public bool IsEmpty => Inner.Count > 0; 35 | 36 | public Keyword Build() => new Keyword { Items = Inner.ToArray() }; 37 | 38 | public static Keyword FromString(string source) 39 | { 40 | return new Keyword { Items = source.Split(';', StringSplitOptions.RemoveEmptyEntries) }; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/AcBlog.Services/AcBlog.Services.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | enable 6 | true 7 | 8 | 9 | StardustDL 10 | AcBlog 11 | Apache-2.0 12 | https://github.com/acblog 13 | https://github.com/acblog/acblog 14 | Git 15 | icon.png 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | True 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.WebAssembly/AcBlog.Client.WebAssembly.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | service-worker-assets.js 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/AcBlog.Services.FileSystem/AcBlog.Services.FileSystem.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | enable 6 | true 7 | 8 | 9 | StardustDL 10 | AcBlog 11 | Apache-2.0 12 | https://github.com/acblog 13 | https://github.com/acblog/acblog 14 | Git 15 | icon.png 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | True 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/AcBlog.Services.SqlServer/AcBlog.Services.SqlServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | enable 6 | true 7 | 8 | 9 | StardustDL 10 | AcBlog 11 | Apache-2.0 12 | https://github.com/acblog 13 | https://github.com/acblog/acblog 14 | Git 15 | icon.png 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | True 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Shared/IconNames.cs: -------------------------------------------------------------------------------- 1 | namespace AcBlog.Client.UI.Shared 2 | { 3 | public static class IconNames 4 | { 5 | public const string Notes = "file-document"; 6 | 7 | public const string Slides = "presentation"; 8 | 9 | public const string Posts = "post"; 10 | 11 | public const string Articles = "feather"; 12 | 13 | public const string Category = "folder"; 14 | 15 | public const string Keywords = "tag-multiple"; 16 | 17 | public const string Comment = "comment"; 18 | 19 | public const string Archive = "archive"; 20 | 21 | public const string Create = "plus"; 22 | 23 | public const string Search = "magnify"; 24 | 25 | public const string Settings = "cog"; 26 | 27 | public const string Page = "book-open"; 28 | 29 | public const string Delete = "delete"; 30 | 31 | public const string Edit = "square-edit-outline"; 32 | 33 | public const string Revert = "backup-restore"; 34 | 35 | public const string Login = "login"; 36 | 37 | public const string Logout = "logout"; 38 | 39 | public const string Key = "key"; 40 | 41 | public const string Home = "home"; 42 | 43 | public const string Account = "account"; 44 | 45 | public const string ChangePassword = "form-textbox-password"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Areas/Identity/Pages/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using AcBlog.Server.Api.Models 3 | @inject SignInManager SignInManager 4 | @inject UserManager UserManager 5 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 6 | 7 | @{ 8 | var returnUrl = "/"; 9 | if (Context.Request.Query.TryGetValue("returnUrl", out var existingUrl)) { 10 | returnUrl = existingUrl; 11 | } 12 | } 13 | 14 | 36 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Pages/Posts/Keywords/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/keywords" 2 | @inherits BaseKeywordPage 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Home 11 | 12 | 13 | 14 | 15 | 16 | Keywords 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | @foreach (var v in Data.Items) 25 | { 26 | 27 | @v.Items.FirstOrDefault() 28 | 29 | } 30 |
31 |
32 |
33 |
34 | 35 | @code { 36 | private KeywordCollection Data; 37 | 38 | private Loader loader; 39 | 40 | private async Task OnLoad() 41 | { 42 | Data = await Service.PostService.GetKeywords(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.FileSystem/AcBlog.Data.Repositories.FileSystem.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | enable 6 | true 7 | 8 | 9 | StardustDL 10 | AcBlog 11 | Apache-2.0 12 | https://github.com/acblog 13 | https://github.com/acblog/acblog 14 | Git 15 | icon.png 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | True 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace AcBlog.Server.Api 9 | { 10 | public class Program 11 | { 12 | public static async Task Main(string[] args) 13 | { 14 | var host = CreateHostBuilder(args).Build(); 15 | 16 | using (var scope = host.Services.CreateScope()) 17 | { 18 | var services = scope.ServiceProvider; 19 | 20 | try 21 | { 22 | await SeedData.InitializeIdentityDb(services); 23 | await SeedData.InitializeDb(services); 24 | } 25 | catch (Exception ex) 26 | { 27 | var logger = services.GetRequiredService>(); 28 | logger.LogError(ex, "An error occurred seeding the DB."); 29 | } 30 | } 31 | 32 | await host.RunAsync(); 33 | } 34 | 35 | public static IHostBuilder CreateHostBuilder(string[] args) => 36 | Host.CreateDefaultBuilder(args) 37 | .ConfigureWebHostDefaults(webBuilder => 38 | { 39 | webBuilder.UseStartup(); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Commands/Remotes/RemoveCommand.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Tools.Sdk.Models; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using System.CommandLine; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace AcBlog.Tools.Sdk.Commands.Remotes 9 | { 10 | public class RemoveCommand : BaseCommand 11 | { 12 | public override string Name => "remove"; 13 | 14 | public override string Description => ""; 15 | 16 | public override Command Configure() 17 | { 18 | var result = base.Configure(); 19 | result.AddArgument(new Argument(nameof(CArgument.Name).ToLowerInvariant(), "Remote server name")); 20 | return result; 21 | } 22 | 23 | public override async Task Handle(CArgument argument, IHost host, CancellationToken cancellationToken) 24 | { 25 | Workspace workspace = host.Services.GetRequiredService(); 26 | workspace.Option.Remotes.Remove(argument.Name); 27 | if (workspace.Option.CurrentRemote == argument.Name) 28 | workspace.Option.CurrentRemote = string.Empty; 29 | await workspace.Save(); 30 | return 0; 31 | } 32 | 33 | public class CArgument 34 | { 35 | public string Name { get; set; } = string.Empty; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Extensions/BlogServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models.Actions; 2 | using AcBlog.Services.Models; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace AcBlog.Services.Extensions 7 | { 8 | public static class BlogServiceExtensions 9 | { 10 | public static Task> QuerySitemap(this IBlogService service, string baseAddress, CancellationToken cancellationToken = default) 11 | { 12 | return service.Query(new Models.BlogQueryRequest 13 | { 14 | Type = BlogQueryRequestStrings.Sitemap, 15 | Data = new System.Collections.Generic.Dictionary 16 | { 17 | [BlogQueryRequestStrings.BaseAddress] = baseAddress 18 | } 19 | }, cancellationToken); 20 | } 21 | 22 | public static Task> QueryAtomFeed(this IBlogService service, string baseAddress, CancellationToken cancellationToken = default) 23 | { 24 | return service.Query(new Models.BlogQueryRequest 25 | { 26 | Type = BlogQueryRequestStrings.AtomFeed, 27 | Data = new System.Collections.Generic.Dictionary 28 | { 29 | [BlogQueryRequestStrings.BaseAddress] = baseAddress 30 | } 31 | }, cancellationToken); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/AcBlog.Sdk/AcBlog.Sdk.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | enable 6 | true 7 | 8 | 9 | StardustDL 10 | AcBlog 11 | Apache-2.0 12 | https://github.com/acblog 13 | https://github.com/acblog/acblog 14 | Git 15 | AcBlog.Sdk 16 | AcBlog.Sdk 17 | icon.png 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | True 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Builders/CategoryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace AcBlog.Data.Models.Builders 5 | { 6 | public class CategoryBuilder 7 | { 8 | List Inner { get; set; } = new List(); 9 | 10 | public CategoryBuilder AddSubCategory(params string[] names) 11 | { 12 | foreach (var name in names) 13 | { 14 | if (!name.Contains('/')) 15 | { 16 | Inner.Add(name); 17 | } 18 | else 19 | { 20 | throw new Exception($"Invalid category name: {name}."); 21 | } 22 | } 23 | return this; 24 | } 25 | 26 | public CategoryBuilder RemoveSubCategory() 27 | { 28 | if (Inner.Count > 0) 29 | { 30 | Inner.RemoveAt(Inner.Count - 1); 31 | return this; 32 | } 33 | else 34 | { 35 | throw new Exception($"Empty category."); 36 | } 37 | } 38 | 39 | public bool IsEmpty => Inner.Count > 0; 40 | 41 | public Category Build() => new Category { Items = Inner.ToArray() }; 42 | 43 | public static Category FromString(string source) 44 | { 45 | return new Category { Items = source.Split('/', StringSplitOptions.RemoveEmptyEntries) }; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/CategoryTree.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AcBlog.Data.Models 4 | { 5 | public class CategoryTree 6 | { 7 | public CategoryTree() : this(new CategoryTreeNode()) { } 8 | 9 | public CategoryTree(CategoryTreeNode root) => Root = root; 10 | 11 | public CategoryTreeNode Root { get; set; } 12 | 13 | public class CategoryTreeNode 14 | { 15 | public CategoryTreeNode() : this(Category.Empty) 16 | { 17 | } 18 | 19 | public CategoryTreeNode(Category category) 20 | { 21 | Category = category; 22 | } 23 | 24 | public Category Category { get; set; } 25 | 26 | public Dictionary Children { get; set; } = new Dictionary(); 27 | } 28 | 29 | public IEnumerable AsCategoryList() 30 | { 31 | Queue q = new Queue(); 32 | foreach (var item in Root.Children.Values) 33 | { 34 | q.Enqueue(item); 35 | } 36 | while (q.Count > 0) 37 | { 38 | var u = q.Dequeue(); 39 | yield return u.Category; 40 | foreach (var v in u.Children.Values) 41 | { 42 | q.Enqueue(v); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.UI/Components/PageContainer.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | @ChildContent 8 | 9 | 10 | @code { 11 | private RenderFragment _breadcrumb; 12 | private RenderFragment _extra; 13 | private RenderFragment _content; 14 | private RenderFragment _tags; 15 | 16 | [Parameter] 17 | public string PageTitle { get; set; } 18 | 19 | [Parameter] 20 | public string Title { get; set; } 21 | 22 | 23 | [Parameter] 24 | public RenderFragment ChildContent { get; set; } 25 | 26 | [Parameter] 27 | public RenderFragment Content 28 | { 29 | get => _content ?? (builder => { }); 30 | set => _content = value; 31 | } 32 | 33 | [Parameter] 34 | public RenderFragment Tags 35 | { 36 | get => _tags ?? (builder => { }); 37 | set => _tags = value; 38 | } 39 | 40 | 41 | [Parameter] 42 | public RenderFragment Breadcrumb 43 | { 44 | get => _breadcrumb ?? (builder => { }); 45 | set => _breadcrumb = value; 46 | } 47 | 48 | [Parameter] 49 | public RenderFragment Extra 50 | { 51 | get => _extra ?? (builder => { }); 52 | set => _extra = value; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/AcBlog.Services/Generators/ClientUriGenerator.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using System; 3 | 4 | namespace AcBlog.Services.Generators 5 | { 6 | public class ClientUriGenerator : IClientUriGenerator 7 | { 8 | private string _baseAddress = string.Empty; 9 | 10 | public string BaseAddress { get => _baseAddress; set => _baseAddress = value.TrimEnd('/'); } 11 | 12 | public string Post(Post post) => $"{BaseAddress}/posts/{Uri.EscapeDataString(post.Id)}"; 13 | 14 | public string Search(string query = "") => $"{BaseAddress}/search/{Uri.EscapeDataString(query)}"; 15 | 16 | public string Posts() => $"{BaseAddress}/posts"; 17 | 18 | public string Articles() => $"{BaseAddress}/articles"; 19 | 20 | public string Slides() => $"{BaseAddress}/slides"; 21 | 22 | public string Notes() => $"{BaseAddress}/notes"; 23 | 24 | public string Archives() => $"{BaseAddress}/archives"; 25 | 26 | public string Categories() => $"{BaseAddress}/categories"; 27 | 28 | public string Comments() => $"{BaseAddress}/comments"; 29 | 30 | public string Category(Category category) => $"{BaseAddress}/categories/{Uri.EscapeDataString(category.ToString())}"; 31 | 32 | public string Keywords() => $"{BaseAddress}/keywords"; 33 | 34 | public string Keyword(Keyword keyword) => $"{BaseAddress}/keywords/{Uri.EscapeDataString(keyword.ToString())}"; 35 | 36 | public string Page(Page page) => $"{BaseAddress}/{page.Route}"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 9 | WORKDIR /src 10 | COPY ["src/AcBlog.Data/AcBlog.Data.csproj", "src/AcBlog.Data/"] 11 | COPY ["src/AcBlog.Sdk/AcBlog.Sdk.csproj", "src/AcBlog.Sdk/"] 12 | COPY ["src/AcBlog.Data.Repositories.FileSystem/AcBlog.Data.Repositories.FileSystem.csproj", "src/AcBlog.Data.Repositories.FileSystem/"] 13 | COPY ["src/client/AcBlog.Client.Core/AcBlog.Client.Core.csproj", "src/client/AcBlog.Client.Core/"] 14 | COPY ["src/client/AcBlog.Client.UI/AcBlog.Client.UI.csproj", "src/client/AcBlog.Client.UI/"] 15 | COPY ["src/client/AcBlog.Client.Server/AcBlog.Client.Server.csproj", "src/client/AcBlog.Client.Server/"] 16 | RUN dotnet restore "src/client/AcBlog.Client.Server/AcBlog.Client.Server.csproj" -s https://sparkshine.pkgs.visualstudio.com/StardustDL/_packaging/feed/nuget/v3/index.json -s https://api.nuget.org/v3/index.json 17 | COPY . . 18 | WORKDIR "/src/src/client/AcBlog.Client.Server" 19 | RUN dotnet build "AcBlog.Client.Server.csproj" -c Release -o /app/build 20 | 21 | FROM build AS publish 22 | RUN dotnet publish "AcBlog.Client.Server.csproj" -c Release -o /app/publish 23 | 24 | FROM base AS final 25 | WORKDIR /app 26 | COPY --from=publish /app/publish . 27 | ENTRYPOINT ["dotnet", "AcBlog.Client.Server.dll"] -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/Controllers/SiteController.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Sdk; 2 | using AcBlog.Services; 3 | using AcBlog.Services.Extensions; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Configuration; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Xml; 9 | 10 | namespace AcBlog.Client.Server.Controllers 11 | { 12 | [Route("[controller]")] 13 | [ApiController] 14 | public class SiteController : ControllerBase 15 | { 16 | private IBlogService BlogService { get; } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | private string BaseAddress { get; } 21 | 22 | public SiteController(IBlogService blogService, IConfiguration configuration) 23 | { 24 | BlogService = blogService; 25 | Configuration = configuration; 26 | BaseAddress = Configuration.GetBaseAddress().TrimEnd('/'); 27 | } 28 | 29 | [HttpGet("sitemap")] 30 | [HttpGet("sitemap.xml")] 31 | public async Task GetSitemap() 32 | { 33 | var result = await BlogService.QuerySitemap(BaseAddress); 34 | return Content(result.Result, "text/xml"); 35 | } 36 | 37 | [HttpGet("atom")] 38 | [HttpGet("atom.xml")] 39 | public async Task GetAtomFeed() 40 | { 41 | var result = await BlogService.QueryAtomFeed(BaseAddress); 42 | return Content(result.Result, "text/xml"); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/AcBlog.Data.Repositories.SqlServer/Models/RawStatistic.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using System; 3 | 4 | namespace AcBlog.Data.Repositories.SqlServer.Models 5 | { 6 | public class RawStatistic : IHasId 7 | { 8 | public string Id { get; set; } = string.Empty; 9 | 10 | public string Category { get; set; } = string.Empty; 11 | 12 | public string Uri { get; set; } = string.Empty; 13 | 14 | public string Payload { get; set; } = string.Empty; 15 | 16 | public DateTimeOffset CreationTime { get; set; } 17 | 18 | public DateTimeOffset ModificationTime { get; set; } 19 | 20 | public static RawStatistic From(Statistic value) 21 | { 22 | return new RawStatistic 23 | { 24 | Id = value.Id, 25 | Category = value.Category, 26 | Payload = value.Payload, 27 | Uri = value.Uri, 28 | CreationTime = value.CreationTime, 29 | ModificationTime = value.ModificationTime, 30 | }; 31 | } 32 | 33 | public static Statistic To(RawStatistic value) 34 | { 35 | return new Statistic 36 | { 37 | Id = value.Id, 38 | Category = value.Category, 39 | Payload = value.Payload, 40 | Uri = value.Uri, 41 | CreationTime = value.CreationTime, 42 | ModificationTime = value.ModificationTime, 43 | }; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/client/AcBlog.Client.Server/AcBlog.Client.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | aspnet-AcBlog.Client.Server-CF5BF023-A97B-4785-AAD5-8D3E42C2CA48 6 | Linux 7 | ..\..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/StardustDL.Extensions.FileProviders.Http/StardustDL.Extensions.FileProviders.Http.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 8.0 6 | true 7 | 8 | 9 | StardustDL 10 | StardustDL 11 | Apache-2.0 12 | https://github.com/StardustDL/Extensions.FileProviders.Http 13 | https://github.com/StardustDL/Extensions.FileProviders.Http 14 | Git 15 | StardustDL.Extensions.FileProviders.Http 16 | StardustDL.Extensions.FileProviders.Http 17 | File provider for HTTP files for Microsoft.Extensions.FileProviders. 18 | StardustDL.Extensions.FileProviders.Http 19 | StardustDL.Extensions.FileProviders.Http 20 | files; filesystem; http 21 | StardustDL 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Repositories/PageFSRepo.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Tools.Sdk.Models.Text; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Tools.Sdk.Repositories 10 | { 11 | internal class PageFSRepo : RecordFSRepo, IPageRepository 12 | { 13 | public PageFSRepo(string rootPath) : base(rootPath) 14 | { 15 | } 16 | 17 | protected override Task CreateExistedItem(string id, PageMetadata metadata, string content) 18 | { 19 | string path = GetPath(id); 20 | if (string.IsNullOrEmpty(metadata.id)) 21 | metadata.id = id; 22 | if (string.IsNullOrEmpty(metadata.creationTime)) 23 | { 24 | metadata.creationTime = System.IO.File.GetCreationTime(path).ToString(); 25 | } 26 | if (string.IsNullOrEmpty(metadata.modificationTime)) 27 | { 28 | metadata.modificationTime = System.IO.File.GetLastWriteTime(path).ToString(); 29 | } 30 | 31 | var result = metadata.ApplyTo(new Page()); 32 | result = result with { Content = content }; 33 | 34 | return Task.FromResult(result); 35 | } 36 | 37 | protected override Task<(PageMetadata, string)> CreateNewItem(Page value) 38 | { 39 | return Task.FromResult((new PageMetadata(value), value.Content)); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AcBlog.Tools.Sdk/Repositories/LayoutFSRepo.cs: -------------------------------------------------------------------------------- 1 | using AcBlog.Data.Models; 2 | using AcBlog.Data.Models.Actions; 3 | using AcBlog.Data.Repositories; 4 | using AcBlog.Tools.Sdk.Models.Text; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AcBlog.Tools.Sdk.Repositories 10 | { 11 | internal class LayoutFSRepo : RecordFSRepo, ILayoutRepository 12 | { 13 | public LayoutFSRepo(string rootPath) : base(rootPath) 14 | { 15 | } 16 | 17 | protected override Task CreateExistedItem(string id, LayoutMetadata metadata, string content) 18 | { 19 | string path = GetPath(id); 20 | if (string.IsNullOrEmpty(metadata.id)) 21 | metadata.id = id; 22 | if (string.IsNullOrEmpty(metadata.creationTime)) 23 | { 24 | metadata.creationTime = System.IO.File.GetCreationTime(path).ToString(); 25 | } 26 | if (string.IsNullOrEmpty(metadata.modificationTime)) 27 | { 28 | metadata.modificationTime = System.IO.File.GetLastWriteTime(path).ToString(); 29 | } 30 | 31 | var result = metadata.ApplyTo(new Layout()); 32 | result = result with { Template = content }; 33 | 34 | return Task.FromResult(result); 35 | } 36 | 37 | protected override Task<(LayoutMetadata, string)> CreateNewItem(Layout value) 38 | { 39 | return Task.FromResult((new LayoutMetadata(value), value.Template)); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AcBlog.Data/Models/Actions/Pagination.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AcBlog.Data.Models.Actions 4 | { 5 | public record Pagination 6 | { 7 | private int _pageSize = 10; 8 | 9 | public int PageSize 10 | { 11 | get => _pageSize; 12 | init 13 | { 14 | if (value <= 0) value = 10; 15 | _pageSize = value; 16 | } 17 | } 18 | 19 | public int CurrentPage { get; init; } 20 | 21 | public int TotalCount { get; init; } 22 | 23 | public int Offset => PageSize * CurrentPage; 24 | 25 | public int TotalPage => (int)Math.Ceiling((double)TotalCount / PageSize); 26 | 27 | public bool HasPreviousPage => Offset > 0; 28 | 29 | public bool HasNextPage => Offset + PageSize < TotalCount; 30 | 31 | public Pagination PreviousPage() 32 | { 33 | if (!HasPreviousPage) 34 | throw new System.Exception("No previous page"); 35 | return new Pagination 36 | { 37 | PageSize = PageSize, 38 | CurrentPage = CurrentPage - 1, 39 | TotalCount = TotalCount 40 | }; 41 | } 42 | 43 | public Pagination NextPage() 44 | { 45 | if (!HasNextPage) 46 | throw new System.Exception("No next page"); 47 | return new Pagination 48 | { 49 | PageSize = PageSize, 50 | CurrentPage = CurrentPage + 1, 51 | TotalCount = TotalCount 52 | }; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/AcBlog.Server.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "ConnectionStrings": { 11 | "IdentityConnection": "Server=(localdb)\\mssqllocaldb;Database=acblogServerAPI_identity;Trusted_Connection=True;MultipleActiveResultSets=true", 12 | "ServiceConnection": "Server=(localdb)\\mssqllocaldb;Database=acblogServerAPI_service;Trusted_Connection=True;MultipleActiveResultSets=true", 13 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=acblogServerAPI;Trusted_Connection=True;MultipleActiveResultSets=true" 14 | }, 15 | "Options": { 16 | "DisableRegisterNewUser": false 17 | }, 18 | "IdentityServer": { 19 | "Clients": { 20 | "AcBlog.Client.WebAssembly": { 21 | "Profile": "SPA", 22 | "RedirectUri": "https://localhost:8101/authentication/login-callback", 23 | "LogoutUri": "https://localhost:8101/authentication/logout-callback" 24 | }, 25 | "AcBlog.Client.Server": { 26 | "Profile": "SPA", 27 | "RedirectUri": "https://localhost:8201/authentication/login-callback", 28 | "LogoutUri": "https://localhost:8201/authentication/logout-callback" 29 | } 30 | }, 31 | "Key": { 32 | "Type": "Development" 33 | }, 34 | "Options": { 35 | "PublicOrigin": "https://localhost:8001" 36 | } 37 | }, 38 | "Cors": [ 39 | "https://localhost:8101", 40 | "http://localhost:8100", 41 | "https://localhost:8201", 42 | "http://localhost:8200" 43 | ] 44 | } 45 | --------------------------------------------------------------------------------