├── .config └── dotnet-tools.json ├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── .vscode ├── .gitignore ├── launch.json └── tasks.json ├── Blogifier.sln ├── Dockerfile ├── LICENSE ├── README-zh_CN.md ├── README.md ├── SECURITY.md ├── build.cmd ├── build.sh ├── deploy └── .gitignore ├── docker-compose.yml ├── docker.sh ├── docs ├── 01-Authentication.md ├── 02-Database.md ├── 03-Logging.md ├── 04-Localization.md ├── 05-Emails.md └── 06-Newsletters.md ├── global.json ├── publish.cmd ├── publish.sh ├── src ├── Blogifier.Admin │ ├── App.razor │ ├── BlogAuthStateProvider.cs │ ├── Blogifier.Admin.csproj │ ├── Components │ │ ├── CategoriesComponent.razor │ │ ├── NavMenuComponent.razor │ │ ├── PageTitleComponent.razor │ │ ├── PostEditorComponent.razor │ │ └── RedirectComponent.razor │ ├── Dtos │ │ ├── FrontBlobInfo.cs │ │ ├── FrontCategoryItemDto.cs │ │ ├── FrontImportDto.cs │ │ ├── FrontPostImportDto.cs │ │ ├── FrontPostItemDto.cs │ │ └── FrontUserInfoDto.cs │ ├── Interop │ │ ├── CommonJsInterop.cs │ │ └── EditorJsInterop.cs │ ├── Pages │ │ ├── Blogs │ │ │ ├── BlogsView.razor │ │ │ ├── CategoryEditorView.razor │ │ │ ├── CategoryView.razor │ │ │ ├── EditorView.razor │ │ │ ├── ImportView.razor │ │ │ └── SettingsView.razor │ │ ├── Drive │ │ │ └── DriveView.razor │ │ ├── HomeView.razor │ │ ├── Newsletter │ │ │ ├── NewsletterView.razor │ │ │ ├── SettingsView.razor │ │ │ └── SubscribersView.razor │ │ ├── Pages │ │ │ ├── EditorView.razor │ │ │ └── PagesView.razor │ │ └── Settings │ │ │ ├── AboutView.razor │ │ │ ├── AdvancedView.razor │ │ │ ├── BasicView.razor │ │ │ ├── CommentsView.razor │ │ │ ├── CustomizeView.razor │ │ │ ├── MenusView.razor │ │ │ ├── ScriptsView.razor │ │ │ ├── ThemesView.razor │ │ │ ├── Users │ │ │ ├── EditorView.razor │ │ │ └── UsersView.razor │ │ │ └── WidgetsView.razor │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Services │ │ └── ToasterService.cs │ ├── Shared │ │ ├── BlogsLayout.razor │ │ ├── MainLayout.razor │ │ ├── NewsletterLayout.razor │ │ └── SettingsLayout.razor │ ├── _Imports.razor │ ├── assets │ │ ├── gulpfile.mjs │ │ ├── js │ │ │ ├── blogifier.js │ │ │ ├── common.js │ │ │ └── editor.js │ │ ├── package.json │ │ └── scss │ │ │ ├── blogifier.scss │ │ │ ├── helpers │ │ │ ├── _bootstrap.scss │ │ │ ├── _colors.scss │ │ │ ├── _mixins.scss │ │ │ ├── _reset.scss │ │ │ └── _variables.scss │ │ │ ├── include │ │ │ ├── _blazor.scss │ │ │ ├── _buttons.scss │ │ │ ├── _editor.scss │ │ │ ├── _forms.scss │ │ │ ├── _highlight.scss │ │ │ ├── _icons.scss │ │ │ ├── _list.scss │ │ │ ├── _section.scss │ │ │ ├── _toaster.scss │ │ │ └── _tooltips.scss │ │ │ ├── layout │ │ │ ├── _main.scss │ │ │ ├── _nav.scss │ │ │ └── _sidebar.scss │ │ │ └── pages │ │ │ ├── account │ │ │ └── _account.scss │ │ │ ├── dashboard │ │ │ └── _dashboard.scss │ │ │ ├── editor │ │ │ └── _editor.scss │ │ │ └── settings │ │ │ ├── _about.scss │ │ │ ├── _customize.scss │ │ │ ├── _settings.scss │ │ │ └── _themes.scss │ └── wwwroot │ │ ├── admin │ │ └── favicons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── mstile-70x70.png │ │ │ ├── safari-pinned-tab.svg │ │ │ └── site.webmanifest │ │ └── index.html ├── Blogifier.Shared │ ├── Blogifier.Shared.csproj │ ├── BlogifierSharedConstant.cs │ ├── Dtos │ │ ├── AboutDto.cs │ │ ├── AnalyticsDto.cs │ │ ├── BlogEitorDto.cs │ │ ├── BlogSumDto.cs │ │ ├── CategoryDto.cs │ │ ├── CategoryEitorDto.cs │ │ ├── CategoryItemDto.cs │ │ ├── ImportDto.cs │ │ ├── ImportRssDto.cs │ │ ├── MailSettingDto.cs │ │ ├── MainDto.cs │ │ ├── NewsletterDto.cs │ │ ├── PostBriefDto.cs │ │ ├── PostDto.cs │ │ ├── PostEditorDto.cs │ │ ├── PostItemDto.cs │ │ ├── PostPagerDto.cs │ │ ├── PostSlugDto.cs │ │ ├── PostToHtmlDto.cs │ │ ├── StorageDto.cs │ │ ├── SubscriberApplyDto.cs │ │ ├── SubscriberDto.cs │ │ ├── UserDto.cs │ │ ├── UserEditorDto.cs │ │ └── UserInfoDto.cs │ ├── Enums │ │ ├── AnalyticsListType.cs │ │ ├── AnalyticsPeriod.cs │ │ ├── GroupAction.cs │ │ ├── PostListType.cs │ │ ├── PostState.cs │ │ ├── PostType.cs │ │ ├── PublishedStatus.cs │ │ ├── SaveStatus.cs │ │ ├── SendNewsletterState.cs │ │ ├── StorageReferenceType.cs │ │ ├── StorageType.cs │ │ ├── UploadType.cs │ │ ├── UserState.cs │ │ └── UserType.cs │ ├── Extensions │ │ └── PrincipalExtensions.cs │ ├── Helper │ │ ├── DateTimeHelper.cs │ │ ├── PageHelper.cs │ │ └── StringHelper.cs │ ├── Identity │ │ ├── BlogifierClaimTypes.cs │ │ └── BlogifierClaims.cs │ ├── Models │ │ ├── AccountInitializeModel.cs │ │ ├── AccountLoginModel.cs │ │ ├── AccountModel.cs │ │ ├── AccountProfileEditModel.cs │ │ ├── AccountProfileModel.cs │ │ ├── AccountProfilePasswordModel.cs │ │ ├── AccountRegisterModel.cs │ │ ├── CategoryModel.cs │ │ ├── ChartModel.cs │ │ ├── IndexModel.cs │ │ ├── MainModel.cs │ │ ├── OptionItem.cs │ │ ├── PostModel.cs │ │ ├── PostPagerModel.cs │ │ ├── PostVisit.cs │ │ ├── SearchModel.cs │ │ ├── ThemeItem.cs │ │ └── ThemeSettings.cs │ └── Resources │ │ ├── Resource.Designer.cs │ │ ├── Resource.bn.resx │ │ ├── Resource.el-GR.resx │ │ ├── Resource.es.resx │ │ ├── Resource.fa.resx │ │ ├── Resource.pt-BR.resx │ │ ├── Resource.resx │ │ ├── Resource.ru.resx │ │ ├── Resource.sv-SE.resx │ │ ├── Resource.ur-PK.resx │ │ ├── Resource.vi-VN.resx │ │ ├── Resource.zh-CN.resx │ │ └── Resource.zh-TW.resx ├── Blogifier.Themes.Standard │ ├── Blogifier.Themes.Standard.csproj │ ├── ThemesStandardConstant.cs │ ├── Views │ │ ├── Themes │ │ │ └── standard │ │ │ │ ├── 404.cshtml │ │ │ │ ├── category.cshtml │ │ │ │ ├── components │ │ │ │ ├── footer.cshtml │ │ │ │ ├── header.cshtml │ │ │ │ ├── nav.cshtml │ │ │ │ ├── newsletter.cshtml │ │ │ │ └── pagination.cshtml │ │ │ │ ├── index.cshtml │ │ │ │ ├── initialize.cshtml │ │ │ │ ├── layouts │ │ │ │ ├── _account.cshtml │ │ │ │ ├── _base.cshtml │ │ │ │ ├── _main.cshtml │ │ │ │ └── _profile.cshtml │ │ │ │ ├── login.cshtml │ │ │ │ ├── page.cshtml │ │ │ │ ├── password.cshtml │ │ │ │ ├── post.cshtml │ │ │ │ ├── post │ │ │ │ ├── author.cshtml │ │ │ │ ├── comments.cshtml │ │ │ │ ├── featured.cshtml │ │ │ │ ├── nav.cshtml │ │ │ │ ├── related.cshtml │ │ │ │ ├── share.cshtml │ │ │ │ ├── view-grid.cshtml │ │ │ │ └── view-list.cshtml │ │ │ │ ├── profile.cshtml │ │ │ │ ├── register.cshtml │ │ │ │ └── search.cshtml │ │ └── _ViewImports.cshtml │ ├── assets │ │ ├── README.md │ │ ├── gulpfile.mjs │ │ ├── js │ │ │ ├── blogifier.js │ │ │ ├── highlight.js │ │ │ ├── main.js │ │ │ └── profile.js │ │ ├── package.json │ │ ├── screenshot.png │ │ ├── scss │ │ │ ├── account.scss │ │ │ ├── base.scss │ │ │ ├── blogifier.scss │ │ │ ├── components │ │ │ │ ├── _dropdowns.scss │ │ │ │ ├── _highlight.scss │ │ │ │ ├── _newsletter.scss │ │ │ │ ├── _pagination.scss │ │ │ │ └── _search.scss │ │ │ ├── helpers │ │ │ │ ├── _base.scss │ │ │ │ ├── _colors.scss │ │ │ │ ├── _mixins.scss │ │ │ │ ├── _reset.scss │ │ │ │ └── _variables.scss │ │ │ ├── include │ │ │ │ ├── _buttons.scss │ │ │ │ └── _forms.scss │ │ │ ├── layout │ │ │ │ ├── _footer.scss │ │ │ │ ├── _header.scss │ │ │ │ ├── _nav.scss │ │ │ │ ├── _sidebar.scss │ │ │ │ └── _widgets.scss │ │ │ ├── page │ │ │ │ ├── _account.scss │ │ │ │ ├── _home.scss │ │ │ │ └── _profile.scss │ │ │ └── post │ │ │ │ ├── _featured.scss │ │ │ │ ├── _nav.scss │ │ │ │ ├── _post.scss │ │ │ │ ├── _related.scss │ │ │ │ ├── _share.scss │ │ │ │ ├── _view-grid.scss │ │ │ │ └── _view-list.scss │ │ ├── settings.json │ │ └── svg │ │ │ ├── bi-arrow-left.svg │ │ │ ├── bi-arrow-right-short.svg │ │ │ ├── bi-arrow-right.svg │ │ │ ├── bi-arrow-up-circle.svg │ │ │ ├── bi-calendar-event.svg │ │ │ ├── bi-chevron-down.svg │ │ │ ├── bi-envelope-fill.svg │ │ │ ├── bi-hash.svg │ │ │ ├── bi-pencil.svg │ │ │ ├── bi-share.svg │ │ │ ├── bi-x-circle.svg │ │ │ ├── box-arrow-in-right.svg │ │ │ ├── categories.svg │ │ │ ├── chevron-down.svg │ │ │ ├── facebook.svg │ │ │ ├── github.svg │ │ │ ├── instagram.svg │ │ │ ├── logo.svg │ │ │ ├── search.svg │ │ │ ├── twitter.svg │ │ │ └── youtube.svg │ └── wwwroot │ │ └── img │ │ ├── logo-black.png │ │ └── logo-white.png └── Blogifier │ ├── Blogifier.csproj │ ├── BlogifierConstant.cs │ ├── Blogs │ ├── AnalyticsProvider.cs │ ├── BlogData.cs │ ├── BlogManager.cs │ ├── BlogNotIitializeException.cs │ └── MainMamager.cs │ ├── Caches │ ├── CacheExtensions.cs │ └── CacheKeys.cs │ ├── Controllers │ ├── AccountController.cs │ ├── AdminController.cs │ ├── CategoryController.cs │ ├── ErrorController.cs │ ├── FeedController.cs │ ├── HomeController.cs │ ├── PageController.cs │ ├── PostController.cs │ ├── SearchController.cs │ ├── SitemapController.cs │ └── StorageController.cs │ ├── Data │ ├── AppDbContext.cs │ ├── AppDbContextExtensions.cs │ ├── AppEntity.cs │ ├── AppProvider.cs │ ├── Migrations │ │ ├── MySql │ │ │ ├── 20230731071404_Init.Designer.cs │ │ │ ├── 20230731071404_Init.cs │ │ │ └── MySqlDbContextModelSnapshot.cs │ │ ├── Postgres │ │ │ ├── 20230609053331_Init.Designer.cs │ │ │ ├── 20230609053331_Init.cs │ │ │ └── PostgresDbContextModelSnapshot.cs │ │ ├── SqlServer │ │ │ ├── 20230609053149_Init.Designer.cs │ │ │ ├── 20230609053149_Init.cs │ │ │ └── SqlServerDbContextModelSnapshot.cs │ │ └── Sqlite │ │ │ ├── 20230609052615_Init.Designer.cs │ │ │ ├── 20230609052615_Init.cs │ │ │ ├── 20230612131808_Storage.Designer.cs │ │ │ ├── 20230612131808_Storage.cs │ │ │ └── SqliteDbContextModelSnapshot.cs │ ├── MySqlDbContext.cs │ ├── PostgresDbContext.cs │ ├── SqlServerDbContext.cs │ ├── SqliteDbContext.cs │ └── ValueGeneration │ │ └── UtcDateTimeValueGenerator .cs │ ├── Extensions │ ├── PrincipalExtensions.cs │ └── StringExtensions.cs │ ├── Identity │ ├── IdentityExtensions.cs │ ├── SignInManager.cs │ ├── UserClaimsPrincipalFactory.cs │ ├── UserInfo.cs │ ├── UserManager.cs │ └── UserProvider.cs │ ├── Interfaces │ ├── AnalyticsController.cs │ ├── BlogController.cs │ ├── CategoryController.cs │ ├── ImportController.cs │ ├── MailController.cs │ ├── NewsletterController.cs │ ├── PostController.cs │ ├── StorageController.cs │ ├── SubscriberController.cs │ ├── TokenController.cs │ └── UserController.cs │ ├── Newsletters │ ├── EmailManager.cs │ ├── MailSettingData.cs │ ├── Newsletter.cs │ ├── NewsletterProvider.cs │ ├── Subscriber.cs │ └── SubscriberProvider.cs │ ├── Options │ ├── OptionInfo.cs │ └── OptionProvider.cs │ ├── Posts │ ├── Category.cs │ ├── CategoryProvider.cs │ ├── ImportManager.cs │ ├── ImportRssProvider.cs │ ├── MarkdigProvider.cs │ ├── Post.cs │ ├── PostCategory.cs │ ├── PostManager.cs │ ├── PostProvider.cs │ ├── PostSearch.cs │ └── ReverseProvider.cs │ ├── Profiles │ ├── BlogProfile.cs │ ├── CategoryProfile.cs │ ├── MailSettingProfile.cs │ ├── NewsletterProfile.cs │ ├── PostProfile.cs │ ├── StorageProfile.cs │ ├── SubscriberProfile.cs │ └── UserProfile.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Storages │ ├── IStorageProvider.cs │ ├── Storage.cs │ ├── StorageExtensions.cs │ ├── StorageLocalProvider.cs │ ├── StorageManager.cs │ ├── StorageMinioProvider.cs │ └── StorageReference.cs │ ├── Views │ ├── 404.cshtml │ ├── Index.cshtml │ └── _ViewImports.cshtml │ ├── appsettings.json │ └── wwwroot │ ├── favicon.ico │ └── img │ ├── avatar.jpg │ └── cover.jpg └── tests └── Blogifier.Tests ├── BaseTests.cs ├── Blogifier.Tests.csproj └── TestHelper.cs /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "7.0.5", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | **/bin 3 | **/obj 4 | **/node_modules 5 | Dockerfile 6 | docker-compose.yml 7 | deploy/ 8 | **/package-lock.json 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 14 | 15 | **Screenshots** 16 | 19 | 20 | **Additional context** 21 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build release test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-linux: 10 | runs-on: ubuntu-latest 11 | steps: 12 | 13 | - name: Checkout 14 | uses: actions/checkout@v2.3.1 15 | 16 | - name: Get npm cache directory 17 | id: npm-cache-dir 18 | shell: bash 19 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} 20 | 21 | - uses: actions/cache@v3 22 | id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true' 23 | with: 24 | path: ${{ steps.npm-cache-dir.outputs.dir }} 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node 28 | 29 | - uses: actions/cache@v3 30 | with: 31 | path: ~/.nuget/packages 32 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} 33 | restore-keys: | 34 | ${{ runner.os }}-nuget- 35 | 36 | - name: Setup .NET 37 | uses: actions/setup-dotnet@v3 38 | with: 39 | dotnet-version: '7.0.x' 40 | 41 | - name: Publish Blogifier 42 | run: dotnet publish -c Release /p:RuntimeIdentifier=linux-x64 ./src/Blogifier/Blogifier.csproj --output dist 43 | 44 | #- name: Add .nojekyll file 45 | # run: touch dist/.nojekyll 46 | 47 | #- name: Deploy 48 | # uses: JamesIves/github-pages-deploy-action@4.1.4 49 | # with: 50 | # branch: demo 51 | # folder: release 52 | -------------------------------------------------------------------------------- /.vscode/.gitignore: -------------------------------------------------------------------------------- 1 | settings.json 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch and Debug", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "program": "dotnet", 12 | "args": [ 13 | "watch" 14 | ], 15 | "cwd": "${workspaceFolder}/src/Blogifier", 16 | "env": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | }, 19 | "launchBrowser": { 20 | "enabled": true, 21 | "args": "${auto-detect-url}", 22 | "windows": { 23 | "command": "cmd.exe", 24 | "args": "/C start ${auto-detect-url}", 25 | }, 26 | "osx": { 27 | "command": "open" 28 | }, 29 | "linux": { 30 | "command": "xdg-open" 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/Blogifier/Blogifier.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/Blogifier/Blogifier.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/src/Blogifier/Blogifier.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine as sdk 2 | # TOTO zh-CH 3 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories 4 | RUN apk add --no-cache npm 5 | # Copy everything else and build 6 | COPY ./ /opt/blogifier 7 | WORKDIR /opt/blogifier 8 | RUN ["dotnet","publish", "-c", "Release","/p:RuntimeIdentifier=linux-musl-x64", "./src/Blogifier/Blogifier.csproj","-o","dist" ] 9 | 10 | FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine as run 11 | # TOTO zh-CH 12 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories 13 | RUN apk add --no-cache icu-libs 14 | ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false 15 | COPY --from=sdk /opt/blogifier/dist /opt/blogifier/ 16 | WORKDIR /opt/blogifier 17 | ENTRYPOINT ["dotnet", "Blogifier.dll"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rxtur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Security patches will be applied to the most recent version. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 2.9.x.x | Most Current Version| 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please report (suspected) security vulnerabilities to 14 | **[blogifierdotnet@gmail.com](mailto:blogifierdotnet@gmail.com)**. 15 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @REM dotnet clean 2 | dotnet build -c Debug /p:RuntimeIdentifier=linux-x64 ./src/Blogifier/Blogifier.csproj --output dist 3 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | # dotnet clean 2 | dotnet build -c Debug /p:RuntimeIdentifier=win-x64 ./src/Blogifier/Blogifier.csproj --output dist 3 | -------------------------------------------------------------------------------- /deploy/.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | services: 4 | 5 | mysql: 6 | image: mysql:latest 7 | container_name: example_mysql 8 | command: 9 | - --default-authentication-plugin=mysql_native_password 10 | - --character-set-server=utf8mb4 11 | - --collation-server=utf8mb4_unicode_ci 12 | - --lower_case_table_names=1 13 | - --performance_schema=off 14 | restart: always 15 | environment: 16 | TZ: "Asia/Shanghai" 17 | MYSQL_ROOT_PASSWORD: blogifier 18 | volumes: 19 | - ./deploy/data/mysql/data:/var/lib/mysql 20 | 21 | redis: 22 | image: redis:alpine 23 | container_name: example_redis 24 | restart: always 25 | command: --requirepass "blogifier" 26 | volumes: 27 | - ./deploy/data/redis/data:/data 28 | 29 | blogifier: 30 | container_name: example_blogifier 31 | image: dorthl/blogifier:latest 32 | restart: always 33 | build: . 34 | ports: 35 | - '8080:80' 36 | environment: 37 | TZ: 'Asia/Shanghai' 38 | Blogifier__DbProvider: MySql 39 | Blogifier__ConnString: Server=mysql;Database=blogifier;User=root;Password=blogifier; 40 | Blogifier__Redis: redis:6379,password=blogifier,defaultDatabase=0 41 | volumes: 42 | - ./deploy/data/blogifier/app_data:/opt/blogifier/App_Data 43 | mem_limit: 256MB 44 | 45 | -------------------------------------------------------------------------------- /docker.sh: -------------------------------------------------------------------------------- 1 | # docker 2 | docker build -t dorthl/blogifier:latest . 3 | docker push dorthl/blogifier:latest 4 | -------------------------------------------------------------------------------- /docs/01-Authentication.md: -------------------------------------------------------------------------------- 1 | 2 | ### User Login and Registration 3 | On the first login, user gets redirected to the `admin/register` page to register a new account. 4 | Once account created, registration page is disabled and this user becomes a blog owner. 5 | 6 | ### Authentication 7 | The blog posts, including blog themes, are all run as public, server-side rendered MVC site. 8 | 9 | Anything under `admin` is Blazor Web Assembly application and is guarded by custom authentication provider (`BlogAuthenticationStateProvider`). 10 | User password is one-way hashed and saved in the `Authors` table on the back-end. 11 | The salt used to hash password pulled from `appsettings.json` configuration file and should be updated **before** creating user account. 12 | 13 | ``` 14 | "Blogifier": { 15 | ... 16 | "Salt": "SECRET-CHANGE-ME!" 17 | } 18 | ``` -------------------------------------------------------------------------------- /docs/03-Logging.md: -------------------------------------------------------------------------------- 1 | ### Serilog 2 | 3 | Logging done using [Serilog Sink File](https://github.com/serilog/serilog-sinks-file) package. 4 | All logs saved to the `/Logs` folder as configured in the application startup: 5 | 6 | ``` 7 | Log.Logger = new LoggerConfiguration() 8 | .Enrich.FromLogContext() 9 | .WriteTo.File("Logs/log-.txt", rollingInterval: RollingInterval.Day) 10 | .CreateLogger(); 11 | 12 | Log.Warning("Test log"); 13 | ``` -------------------------------------------------------------------------------- /docs/04-Localization.md: -------------------------------------------------------------------------------- 1 | ### Blazor Internationalization(I18n) Text 2 | 3 | Localization and internationalization is done on admin UI level using 4 | [Blazor Internationalization](https://github.com/jsakamoto/Toolbelt.Blazor.I18nText) package. 5 | It uses JSON files instead of standard XML resources used by MS frameworks and compiles resources on build making them strongly typed. 6 | 7 | Resource language files located under `Blogifier.Admin/i18ntext` folder. 8 | If browser set to use one of the cultures in this folder, admin UI should display accordingly. -------------------------------------------------------------------------------- /docs/05-Emails.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/2d8fd528e6d4175e76ff79498a9ff9c5bb078290/docs/05-Emails.md -------------------------------------------------------------------------------- /docs/06-Newsletters.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/2d8fd528e6d4175e76ff79498a9ff9c5bb078290/docs/06-Newsletters.md -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.101" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /publish.cmd: -------------------------------------------------------------------------------- 1 | rmdir dist /s/q 2 | dotnet publish -c Release /p:RuntimeIdentifier=win-x64 ./src/Blogifier/Blogifier.csproj -v minimal --output dist 3 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | # Local machine 2 | rm -fr dist 3 | dotnet publish -c Release /p:RuntimeIdentifier=linux-x64 ./src/Blogifier/Blogifier.csproj --output dist 4 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Sorry, there's nothing at this address.

9 |
10 |
11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/BlogAuthStateProvider.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Identity; 2 | using Microsoft.AspNetCore.Components.Authorization; 3 | using Microsoft.Extensions.Logging; 4 | using System.Net.Http; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace Blogifier.Admin; 9 | 10 | public class BlogAuthStateProvider(ILogger logger, 11 | HttpClient httpClient) : AuthenticationStateProvider 12 | { 13 | private readonly ILogger _logger = logger; 14 | protected readonly HttpClient _httpClient = httpClient; 15 | protected AuthenticationState? _state; 16 | 17 | public override async Task GetAuthenticationStateAsync() 18 | { 19 | if (_state == null) 20 | { 21 | var response = await _httpClient.GetAsync("/api/token/userinfo"); 22 | BlogifierClaims? claims = null; 23 | if (response.IsSuccessStatusCode) 24 | { 25 | var stream = await response.Content.ReadAsStreamAsync(); 26 | if (stream.Length > 0) 27 | { 28 | claims = JsonSerializer.Deserialize(stream, BlogifierSharedConstant.DefaultJsonSerializerOptions)!; 29 | _logger.LogInformation("claims success userName:{UserName}", claims.UserName); 30 | } 31 | } 32 | else 33 | { 34 | _logger.LogError("claims http error StatusCode:{StatusCode}", response.StatusCode); 35 | } 36 | var principal = BlogifierClaims.Generate(claims); 37 | _state = new AuthenticationState(principal); 38 | } 39 | return _state; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Components/CategoriesComponent.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer _localizer 2 | 3 | @code { 4 | [Parameter] public List Categories { get; set; } = default!; 5 | 6 | protected string Tag { get; set; } = default!; 7 | 8 | protected override void OnInitialized() 9 | { 10 | Tag = string.Empty; 11 | } 12 | 13 | protected void KeyPressed(KeyboardEventArgs eventArgs) 14 | { 15 | if (eventArgs.Code == "Enter") 16 | { 17 | Categories.Add(new CategoryDto { Content = Tag }); 18 | Tag = string.Empty; 19 | } 20 | } 21 | 22 | protected void Remove(string tag) 23 | { 24 | Categories!.Remove(Categories.Where(c => c.Content == tag).First()); 25 | } 26 | } 27 | 38 |
39 | @foreach (var item in Categories) 40 | { 41 |
42 | 43 | 45 | 46 | 47 |
48 | } 49 | 50 |
51 | 52 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Components/PageTitleComponent.razor: -------------------------------------------------------------------------------- 1 | @inject IJSRuntime _jsRuntime; 2 | @inject CommonJsInterop _commonJsInterop 3 | 4 | @code { 5 | [Parameter] public string Title { get; set; } = default!; 6 | 7 | protected override async Task OnInitializedAsync() 8 | { 9 | await _commonJsInterop.SetTitleAsync(Title); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Components/RedirectComponent.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager _navigationManager 2 | 3 | @code { 4 | protected override void OnInitialized() 5 | { 6 | _navigationManager.NavigateTo($"account?redirectUri={Uri.EscapeDataString(_navigationManager.Uri)}", true, true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontBlobInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Admin; 2 | 3 | public class FrontBlobInfo 4 | { 5 | public string FileName { get; set; } = default!; 6 | public string Url { get; set; } = default!; 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontCategoryItemDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class FrontCategoryItemDto : CategoryItemDto 4 | { 5 | public bool Selected { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontImportDto.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | using System.Collections.Generic; 3 | 4 | namespace Blogifier.Admin; 5 | 6 | public class FrontImportDto : ImportDto 7 | { 8 | public new List Posts { get; set; } = default!; 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontPostImportDto.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | 3 | namespace Blogifier.Admin; 4 | 5 | public class FrontPostImportDto : PostEditorDto 6 | { 7 | public bool Selected { get; set; } 8 | public bool? ImportComplete { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontPostItemDto.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | 3 | namespace Blogifier.Admin; 4 | 5 | public class FrontPostItemDto : PostItemDto 6 | { 7 | public bool Selected { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontUserInfoDto.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | 3 | namespace Blogifier.Admin; 4 | 5 | public class FrontUserInfoDto : UserInfoDto 6 | { 7 | public bool Selected { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Interop/CommonJsInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.JSInterop; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace Blogifier.Admin.Interop; 7 | 8 | public class CommonJsInterop(IJSRuntime jsRuntime) : IAsyncDisposable 9 | { 10 | private readonly Lazy> moduleTask = new(() => 11 | jsRuntime.InvokeAsync("import", "./admin/js/common.js").AsTask()); 12 | 13 | public async ValueTask SetTooltipAsync() 14 | { 15 | var module = await moduleTask.Value; 16 | await module.InvokeVoidAsync("setTooltip"); 17 | } 18 | 19 | public async ValueTask SetTitleAsync(string content) 20 | { 21 | var module = await moduleTask.Value; 22 | await module.InvokeVoidAsync("setTitle", content); 23 | } 24 | 25 | public async ValueTask TriggerClickAsync(ElementReference? element) 26 | { 27 | var module = await moduleTask.Value; 28 | await module.InvokeVoidAsync("triggerClick", element); 29 | } 30 | 31 | public async ValueTask GetInputFileBlobInfoAsync(ElementReference? inputUpload) 32 | { 33 | var module = await moduleTask.Value; 34 | return await module.InvokeAsync("getInputFileBlobInfo", inputUpload); 35 | } 36 | 37 | public async ValueTask DisposeAsync() 38 | { 39 | if (moduleTask.IsValueCreated) 40 | { 41 | var module = await moduleTask.Value; 42 | await module.DisposeAsync(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Interop/EditorJsInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.JSInterop; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace Blogifier.Admin.Interop; 7 | 8 | public class EditorJsInterop(IJSRuntime jsRuntime) : IAsyncDisposable 9 | { 10 | private readonly Lazy> moduleTask = new(() => 11 | jsRuntime.InvokeAsync("import", "./admin/js/editor.js").AsTask()); 12 | 13 | public async ValueTask LoadEditorAsync(ElementReference? textarea, ElementReference? imageUpload, string toolbar = "fullToolbar") 14 | { 15 | var module = await moduleTask.Value; 16 | await module.InvokeVoidAsync("loadEditor", toolbar, textarea, imageUpload); 17 | } 18 | 19 | public async ValueTask SetEditorValueAsync(string content) 20 | { 21 | var module = await moduleTask.Value; 22 | await module.InvokeVoidAsync("setEditorValue", content); 23 | } 24 | 25 | public async ValueTask GetEditorValueAsync() 26 | { 27 | var module = await moduleTask.Value; 28 | var content = await module.InvokeAsync("getEditorValue"); 29 | return content; 30 | } 31 | 32 | public async ValueTask WriteFrontFileAsync(ElementReference? imageUpload) 33 | { 34 | var module = await moduleTask.Value; 35 | await module.InvokeVoidAsync("writeFrontFile", imageUpload); 36 | } 37 | 38 | public async ValueTask DisposeAsync() 39 | { 40 | if (moduleTask.IsValueCreated) 41 | { 42 | var module = await moduleTask.Value; 43 | await module.DisposeAsync(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Blogs/SettingsView.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/blogs/settings" 2 | @layout BlogsLayout 3 | 4 | @inject HttpClient _http 5 | @inject IStringLocalizer _localizer 6 | @inject IToaster _toaster 7 | 8 | @code { 9 | protected BlogEitorDto? Blog { get; set; } 10 | 11 | protected override async Task OnInitializedAsync() 12 | { 13 | Blog = await _http.GetFromJsonAsync("api/blog"); 14 | } 15 | 16 | protected async Task Save() 17 | { 18 | Toast(await _http.PutAsJsonAsync("api/blog", Blog!)); 19 | } 20 | 21 | protected void Toast(HttpResponseMessage msg) 22 | { 23 | if (msg.IsSuccessStatusCode) 24 | _toaster.Success(_localizer["completed"]); 25 | else 26 | _toaster.Error(_localizer["generic-error"]); 27 | } 28 | } 29 | 30 | 31 | 32 | @if (Blog != null) 33 | { 34 |

@_localizer["blog-settings"]

35 |
36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 |
45 |
46 | 47 | 48 |
49 |
50 | 51 |
52 |
53 |
54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Drive/DriveView.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/drive/" 2 | @inject IStringLocalizer _localizer 3 | 4 | 5 | 6 |
7 |

@_localizer["drive"]

8 |
9 |

@_localizer["drive-describe"]

10 |
11 | @_localizer["under-development"]. 12 | 13 | @_localizer["more-info"] 14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/AdvancedView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/advanced/" 3 | @inject IStringLocalizer _localizer 4 | 5 | 6 | 7 |

@_localizer["advanced-settings"]

8 |
9 |
10 | @_localizer["under-development"]. 11 | 12 | @_localizer["more-info"] 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/CommentsView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/comments/" 3 | @inject HttpClient _http 4 | @inject IStringLocalizer _localizer 5 | @inject IToaster _toaster 6 | 7 | 8 | 9 |

@_localizer["comments-settings"]

10 |
11 |
12 | @_localizer["under-development"]. 13 | 14 | @_localizer["more-info"] 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/CustomizeView.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/settings/customize/" 2 | 3 | @layout SettingsLayout 4 | 5 | @inject HttpClient _http 6 | @inject IStringLocalizer _localizer 7 | @inject IJSRuntime _jsRuntime 8 | @inject IToaster _toaster 9 | 10 | 11 | 12 |

@_localizer["menus"]

13 |

@_localizer["theme-customization-desc"]

14 |
15 |
16 | @_localizer["under-development"]. 17 | 18 | @_localizer["more-info"] 19 | 20 |
21 |
22 | 23 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/MenusView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/customize/menus/" 3 | @inject IStringLocalizer _localizer 4 | 5 | 6 | 7 |

@_localizer["menus"]

8 |

@_localizer["theme-customization-desc"]

9 |
10 |
11 | @_localizer["under-development"]. 12 | 13 | @_localizer["more-info"] 14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/ScriptsView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/scripts/" 3 | @inject HttpClient _http 4 | @inject IToaster _toaster 5 | @inject IStringLocalizer _localizer 6 | 7 | 8 | 9 |

@_localizer["script-settings"]

10 |

@_localizer["include-scripts"]

11 |
12 | @if (Blog != null) 13 | { 14 | 15 |
16 | 17 |