From 48e6d3edfabaea549be8eba7e70780678ca45b4b Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 18 Apr 2022 18:06:17 -0400 Subject: [PATCH] Add page edit - Add nav link and user log-on link filter/tag - Add page list support - Add prior permalink index/search - Remove v2 C# projects --- src/MyWebLog.CS/Db/.gitignore | 1 - .../Features/Admin/AdminController.cs | 53 -- .../Features/Admin/DashboardModel.cs | 40 -- src/MyWebLog.CS/Features/Admin/Index.cshtml | 61 -- .../Features/Admin/Settings.cshtml | 50 -- .../Features/Admin/SettingsModel.cs | 76 --- .../Features/Categories/CategoryController.cs | 29 - src/MyWebLog.CS/Features/FeatureSupport.cs | 67 --- src/MyWebLog.CS/Features/Pages/All.cshtml | 37 -- src/MyWebLog.CS/Features/Pages/Edit.cshtml | 56 -- .../Features/Pages/EditPageModel.cs | 99 ---- .../Features/Pages/PageController.cs | 64 --- .../Features/Pages/PageListModel.cs | 19 - .../Features/Pages/SinglePageModel.cs | 27 - .../Features/Posts/MultiplePostModel.cs | 22 - .../Features/Posts/PostController.cs | 68 --- .../Features/Shared/MyWebLogController.cs | 41 -- .../Features/Shared/MyWebLogModel.cs | 21 - .../Shared/TagHelpers/ImageTagHelper.cs | 37 -- .../Shared/TagHelpers/LinkTagHelper.cs | 55 -- .../Shared/TagHelpers/YesNoTagHelper.cs | 29 - .../Features/Shared/_AdminLayout.cshtml | 44 -- .../Features/Shared/_LogOnOffPartial.cshtml | 11 - src/MyWebLog.CS/Features/ThemeSupport.cs | 28 - src/MyWebLog.CS/Features/Users/LogOn.cshtml | 31 -- src/MyWebLog.CS/Features/Users/LogOnModel.cs | 30 - .../Features/Users/UserController.cs | 76 --- src/MyWebLog.CS/Features/_ViewImports.cshtml | 7 - src/MyWebLog.CS/GlobalUsings.cs | 3 - src/MyWebLog.CS/MyWebLog.CS.csproj | 40 -- src/MyWebLog.CS/Program.cs | 140 ----- .../Properties/Resources.Designer.cs | 459 --------------- src/MyWebLog.CS/Properties/Resources.resx | 252 --------- .../Properties/launchSettings.json | 28 - .../Default/Shared/_DefaultFooter.cshtml | 6 - .../Default/Shared/_DefaultHeader.cshtml | 20 - .../Themes/Default/Shared/_Layout.cshtml | 35 -- .../Themes/Default/SinglePage.cshtml | 10 - src/MyWebLog.CS/Themes/_ViewImports.cshtml | 3 - src/MyWebLog.CS/WebLogMiddleware.cs | 91 --- src/MyWebLog.CS/appsettings.Development.json | 8 - src/MyWebLog.CS/appsettings.json | 9 - src/MyWebLog.CS/wwwroot/css/admin.css | 5 - src/MyWebLog.CS/wwwroot/img/logo-dark.png | Bin 3362 -> 0 bytes src/MyWebLog.CS/wwwroot/img/logo-light.png | Bin 4135 -> 0 bytes src/MyWebLog.Data/Data.fs | 102 +++- src/MyWebLog.DataCS/Category.cs | 42 -- src/MyWebLog.DataCS/Comment.cs | 62 --- src/MyWebLog.DataCS/Enums.cs | 47 -- .../Extensions/CategoryExtensions.cs | 20 - .../Extensions/PageExtensions.cs | 67 --- .../Extensions/PostExtensions.cs | 33 -- .../Extensions/WebLogDetailsExtensions.cs | 22 - .../Extensions/WebLogUserExtensions.cs | 16 - .../20220307034307_Initial.Designer.cs | 521 ------------------ .../Migrations/20220307034307_Initial.cs | 399 -------------- .../WebLogDbContextModelSnapshot.cs | 519 ----------------- src/MyWebLog.DataCS/MyWebLog.DataCS.csproj | 20 - src/MyWebLog.DataCS/Page.cs | 67 --- src/MyWebLog.DataCS/Permalink.cs | 49 -- src/MyWebLog.DataCS/Post.cs | 72 --- src/MyWebLog.DataCS/Revision.cs | 59 -- src/MyWebLog.DataCS/Tag.cs | 17 - src/MyWebLog.DataCS/WebLogDbContext.cs | 87 --- src/MyWebLog.DataCS/WebLogDetails.cs | 42 -- src/MyWebLog.DataCS/WebLogUser.cs | 62 --- src/MyWebLog.Domain/DataTypes.fs | 4 +- src/MyWebLog.Domain/SupportTypes.fs | 8 + src/MyWebLog.Domain/ViewModels.fs | 74 +++ src/MyWebLog.Migrate/Program.fs | 178 ------ src/MyWebLog.Migrate/project.json | 33 -- src/MyWebLog.sln | 12 - src/MyWebLog/Caches.fs | 74 +++ src/MyWebLog/Handlers.fs | 261 ++++++--- src/MyWebLog/MyWebLog.fsproj | 4 +- src/MyWebLog/Program.fs | 56 +- src/MyWebLog/WebLogCache.fs | 24 - src/MyWebLog/themes/admin/dashboard.liquid | 5 +- src/MyWebLog/themes/admin/layout.liquid | 16 +- src/MyWebLog/themes/admin/log-on.liquid | 2 +- src/MyWebLog/themes/admin/page-edit.liquid | 55 ++ src/MyWebLog/themes/admin/page-list.liquid | 27 + src/MyWebLog/themes/admin/settings.liquid | 3 +- src/MyWebLog/themes/default/layout.liquid | 20 +- .../themes/default/single-page.liquid | 2 +- 85 files changed, 593 insertions(+), 4878 deletions(-) delete mode 100644 src/MyWebLog.CS/Db/.gitignore delete mode 100644 src/MyWebLog.CS/Features/Admin/AdminController.cs delete mode 100644 src/MyWebLog.CS/Features/Admin/DashboardModel.cs delete mode 100644 src/MyWebLog.CS/Features/Admin/Index.cshtml delete mode 100644 src/MyWebLog.CS/Features/Admin/Settings.cshtml delete mode 100644 src/MyWebLog.CS/Features/Admin/SettingsModel.cs delete mode 100644 src/MyWebLog.CS/Features/Categories/CategoryController.cs delete mode 100644 src/MyWebLog.CS/Features/FeatureSupport.cs delete mode 100644 src/MyWebLog.CS/Features/Pages/All.cshtml delete mode 100644 src/MyWebLog.CS/Features/Pages/Edit.cshtml delete mode 100644 src/MyWebLog.CS/Features/Pages/EditPageModel.cs delete mode 100644 src/MyWebLog.CS/Features/Pages/PageController.cs delete mode 100644 src/MyWebLog.CS/Features/Pages/PageListModel.cs delete mode 100644 src/MyWebLog.CS/Features/Pages/SinglePageModel.cs delete mode 100644 src/MyWebLog.CS/Features/Posts/MultiplePostModel.cs delete mode 100644 src/MyWebLog.CS/Features/Posts/PostController.cs delete mode 100644 src/MyWebLog.CS/Features/Shared/MyWebLogController.cs delete mode 100644 src/MyWebLog.CS/Features/Shared/MyWebLogModel.cs delete mode 100644 src/MyWebLog.CS/Features/Shared/TagHelpers/ImageTagHelper.cs delete mode 100644 src/MyWebLog.CS/Features/Shared/TagHelpers/LinkTagHelper.cs delete mode 100644 src/MyWebLog.CS/Features/Shared/TagHelpers/YesNoTagHelper.cs delete mode 100644 src/MyWebLog.CS/Features/Shared/_AdminLayout.cshtml delete mode 100644 src/MyWebLog.CS/Features/Shared/_LogOnOffPartial.cshtml delete mode 100644 src/MyWebLog.CS/Features/ThemeSupport.cs delete mode 100644 src/MyWebLog.CS/Features/Users/LogOn.cshtml delete mode 100644 src/MyWebLog.CS/Features/Users/LogOnModel.cs delete mode 100644 src/MyWebLog.CS/Features/Users/UserController.cs delete mode 100644 src/MyWebLog.CS/Features/_ViewImports.cshtml delete mode 100644 src/MyWebLog.CS/GlobalUsings.cs delete mode 100644 src/MyWebLog.CS/MyWebLog.CS.csproj delete mode 100644 src/MyWebLog.CS/Program.cs delete mode 100644 src/MyWebLog.CS/Properties/Resources.Designer.cs delete mode 100644 src/MyWebLog.CS/Properties/Resources.resx delete mode 100644 src/MyWebLog.CS/Properties/launchSettings.json delete mode 100644 src/MyWebLog.CS/Themes/Default/Shared/_DefaultFooter.cshtml delete mode 100644 src/MyWebLog.CS/Themes/Default/Shared/_DefaultHeader.cshtml delete mode 100644 src/MyWebLog.CS/Themes/Default/Shared/_Layout.cshtml delete mode 100644 src/MyWebLog.CS/Themes/Default/SinglePage.cshtml delete mode 100644 src/MyWebLog.CS/Themes/_ViewImports.cshtml delete mode 100644 src/MyWebLog.CS/WebLogMiddleware.cs delete mode 100644 src/MyWebLog.CS/appsettings.Development.json delete mode 100644 src/MyWebLog.CS/appsettings.json delete mode 100644 src/MyWebLog.CS/wwwroot/css/admin.css delete mode 100644 src/MyWebLog.CS/wwwroot/img/logo-dark.png delete mode 100644 src/MyWebLog.CS/wwwroot/img/logo-light.png delete mode 100644 src/MyWebLog.DataCS/Category.cs delete mode 100644 src/MyWebLog.DataCS/Comment.cs delete mode 100644 src/MyWebLog.DataCS/Enums.cs delete mode 100644 src/MyWebLog.DataCS/Extensions/CategoryExtensions.cs delete mode 100644 src/MyWebLog.DataCS/Extensions/PageExtensions.cs delete mode 100644 src/MyWebLog.DataCS/Extensions/PostExtensions.cs delete mode 100644 src/MyWebLog.DataCS/Extensions/WebLogDetailsExtensions.cs delete mode 100644 src/MyWebLog.DataCS/Extensions/WebLogUserExtensions.cs delete mode 100644 src/MyWebLog.DataCS/Migrations/20220307034307_Initial.Designer.cs delete mode 100644 src/MyWebLog.DataCS/Migrations/20220307034307_Initial.cs delete mode 100644 src/MyWebLog.DataCS/Migrations/WebLogDbContextModelSnapshot.cs delete mode 100644 src/MyWebLog.DataCS/MyWebLog.DataCS.csproj delete mode 100644 src/MyWebLog.DataCS/Page.cs delete mode 100644 src/MyWebLog.DataCS/Permalink.cs delete mode 100644 src/MyWebLog.DataCS/Post.cs delete mode 100644 src/MyWebLog.DataCS/Revision.cs delete mode 100644 src/MyWebLog.DataCS/Tag.cs delete mode 100644 src/MyWebLog.DataCS/WebLogDbContext.cs delete mode 100644 src/MyWebLog.DataCS/WebLogDetails.cs delete mode 100644 src/MyWebLog.DataCS/WebLogUser.cs delete mode 100644 src/MyWebLog.Migrate/Program.fs delete mode 100644 src/MyWebLog.Migrate/project.json create mode 100644 src/MyWebLog/Caches.fs delete mode 100644 src/MyWebLog/WebLogCache.fs create mode 100644 src/MyWebLog/themes/admin/page-edit.liquid create mode 100644 src/MyWebLog/themes/admin/page-list.liquid diff --git a/src/MyWebLog.CS/Db/.gitignore b/src/MyWebLog.CS/Db/.gitignore deleted file mode 100644 index 778e729..0000000 --- a/src/MyWebLog.CS/Db/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.db* diff --git a/src/MyWebLog.CS/Features/Admin/AdminController.cs b/src/MyWebLog.CS/Features/Admin/AdminController.cs deleted file mode 100644 index af2be0f..0000000 --- a/src/MyWebLog.CS/Features/Admin/AdminController.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; - -namespace MyWebLog.Features.Admin; - -/// -/// Controller for admin-specific displays and routes -/// -[Route("/admin")] -[Authorize] -public class AdminController : MyWebLogController -{ - /// - public AdminController(WebLogDbContext db) : base(db) { } - - [HttpGet("")] - public async Task Index() => - View(new DashboardModel(WebLog) - { - Posts = await Db.Posts.CountByStatus(PostStatus.Published), - Drafts = await Db.Posts.CountByStatus(PostStatus.Draft), - Pages = await Db.Pages.CountAll(), - ListedPages = await Db.Pages.CountListed(), - Categories = await Db.Categories.CountAll(), - TopLevelCategories = await Db.Categories.CountTopLevel() - }); - - [HttpGet("settings")] - public async Task Settings() => - View(new SettingsModel(WebLog) - { - DefaultPages = Enumerable.Repeat(new SelectListItem($"- {Resources.FirstPageOfPosts} -", "posts"), 1) - .Concat((await Db.Pages.FindAll()).Select(p => new SelectListItem(p.Title, p.Id))) - }); - - [HttpPost("settings")] - public async Task SaveSettings(SettingsModel model) - { - var details = await Db.WebLogDetails.GetByHost(WebLog.UrlBase); - if (details is null) return NotFound(); - - model.PopulateSettings(details); - await Db.SaveChangesAsync(); - - // Update cache - WebLogCache.Set(WebLogCache.HostToDb(HttpContext), (await Db.WebLogDetails.FindByHost(WebLog.UrlBase))!); - - // TODO: confirmation message - - return RedirectToAction(nameof(Index)); - } -} diff --git a/src/MyWebLog.CS/Features/Admin/DashboardModel.cs b/src/MyWebLog.CS/Features/Admin/DashboardModel.cs deleted file mode 100644 index 3307a59..0000000 --- a/src/MyWebLog.CS/Features/Admin/DashboardModel.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace MyWebLog.Features.Admin; - -/// -/// The model used to display the dashboard -/// -public class DashboardModel : MyWebLogModel -{ - /// - /// The number of published posts - /// - public int Posts { get; set; } = 0; - - /// - /// The number of post drafts - /// - public int Drafts { get; set; } = 0; - - /// - /// The number of pages - /// - public int Pages { get; set; } = 0; - - /// - /// The number of pages in the page list - /// - public int ListedPages { get; set; } = 0; - - /// - /// The number of categories - /// - public int Categories { get; set; } = 0; - - /// - /// The top-level categories - /// - public int TopLevelCategories { get; set; } = 0; - - /// - public DashboardModel(WebLogDetails webLog) : base(webLog) { } -} diff --git a/src/MyWebLog.CS/Features/Admin/Index.cshtml b/src/MyWebLog.CS/Features/Admin/Index.cshtml deleted file mode 100644 index 72ef232..0000000 --- a/src/MyWebLog.CS/Features/Admin/Index.cshtml +++ /dev/null @@ -1,61 +0,0 @@ -@model DashboardModel -@{ - Layout = "_AdminLayout"; - ViewBag.Title = Resources.Dashboard; -} -
-
-
-
-
@Resources.Posts
-
-
- @Resources.Published @Model.Posts -   @Resources.Drafts @Model.Drafts -
- @Resources.ViewAll - - @Resources.WriteANewPost - -
-
-
-
-
-
@Resources.Pages
-
-
- @Resources.All @Model.Pages -   @Resources.ShownInPageList @Model.ListedPages -
- @Resources.ViewAll - - @Resources.CreateANewPage - -
-
-
-
-
-
-
-
@Resources.Categories
-
-
- @Resources.All @Model.Categories -   @Resources.TopLevel @Model.TopLevelCategories -
- @Resources.ViewAll - - @Resources.AddANewCategory - -
-
-
-
- -
diff --git a/src/MyWebLog.CS/Features/Admin/Settings.cshtml b/src/MyWebLog.CS/Features/Admin/Settings.cshtml deleted file mode 100644 index 51c19a6..0000000 --- a/src/MyWebLog.CS/Features/Admin/Settings.cshtml +++ /dev/null @@ -1,50 +0,0 @@ -@model SettingsModel -@{ - Layout = "_AdminLayout"; - ViewBag.Title = Resources.WebLogSettings; -} -
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
- -
-
-
-
-
diff --git a/src/MyWebLog.CS/Features/Admin/SettingsModel.cs b/src/MyWebLog.CS/Features/Admin/SettingsModel.cs deleted file mode 100644 index c1cfc7d..0000000 --- a/src/MyWebLog.CS/Features/Admin/SettingsModel.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Rendering; -using System.ComponentModel.DataAnnotations; - -namespace MyWebLog.Features.Admin; - -/// -/// View model for editing web log settings -/// -public class SettingsModel : MyWebLogModel -{ - /// - /// The name of the web log - /// - [Required(AllowEmptyStrings = false)] - [Display(ResourceType = typeof(Resources), Name = "Name")] - public string Name { get; set; } = ""; - - /// - /// The subtitle of the web log - /// - [Display(ResourceType = typeof(Resources), Name = "Subtitle")] - public string Subtitle { get; set; } = ""; - - /// - /// The default page - /// - [Required] - [Display(ResourceType = typeof(Resources), Name = "DefaultPage")] - public string DefaultPage { get; set; } = ""; - - /// - /// How many posts should appear on index pages - /// - [Required] - [Display(ResourceType = typeof(Resources), Name = "PostsPerPage")] - [Range(0, 50)] - public byte PostsPerPage { get; set; } = 10; - - /// - /// The time zone in which dates/times should be displayed - /// - [Required] - [Display(ResourceType = typeof(Resources), Name = "TimeZone")] - public string TimeZone { get; set; } = ""; - - /// - /// Possible values for the default page - /// - public IEnumerable DefaultPages { get; set; } = Enumerable.Empty(); - - [Obsolete("Only used for model binding; use the WebLogDetails constructor")] - public SettingsModel() : base(new()) { } - - /// - public SettingsModel(WebLogDetails webLog) : base(webLog) - { - Name = webLog.Name; - Subtitle = webLog.Subtitle ?? ""; - DefaultPage = webLog.DefaultPage; - PostsPerPage = webLog.PostsPerPage; - TimeZone = webLog.TimeZone; - } - - /// - /// Populate the settings object from the data in this form - /// - /// The settings to be updated - public void PopulateSettings(WebLogDetails settings) - { - settings.Name = Name; - settings.Subtitle = Subtitle == "" ? null : Subtitle; - settings.DefaultPage = DefaultPage; - settings.PostsPerPage = PostsPerPage; - settings.TimeZone = TimeZone; - } -} diff --git a/src/MyWebLog.CS/Features/Categories/CategoryController.cs b/src/MyWebLog.CS/Features/Categories/CategoryController.cs deleted file mode 100644 index 5ad1b81..0000000 --- a/src/MyWebLog.CS/Features/Categories/CategoryController.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace MyWebLog.Features.Categories; - -/// -/// Handle routes for categories -/// -[Route("/category")] -[Authorize] -public class CategoryController : MyWebLogController -{ - /// - public CategoryController(WebLogDbContext db) : base(db) { } - - [HttpGet("all")] - public async Task All() - { - await Task.CompletedTask; - throw new NotImplementedException(); - } - - [HttpGet("{id}/edit")] - public async Task Edit(string id) - { - await Task.CompletedTask; - throw new NotImplementedException(); - } -} diff --git a/src/MyWebLog.CS/Features/FeatureSupport.cs b/src/MyWebLog.CS/Features/FeatureSupport.cs deleted file mode 100644 index 041ebcb..0000000 --- a/src/MyWebLog.CS/Features/FeatureSupport.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Razor; -using System.Collections.Concurrent; -using System.Reflection; - -namespace MyWebLog.Features; - -/// -/// A controller model convention that identifies the feature in which a controller exists -/// -public class FeatureControllerModelConvention : IControllerModelConvention -{ - /// - /// A cache of controller types to features - /// - private static readonly ConcurrentDictionary _features = new(); - - /// - /// Derive the feature name from the controller's type - /// - private static string? GetFeatureName(TypeInfo typ) - { - var cacheKey = typ.FullName ?? ""; - if (_features.ContainsKey(cacheKey)) return _features[cacheKey]; - - var tokens = cacheKey.Split('.'); - if (tokens.Any(it => it == "Features")) - { - var feature = tokens.SkipWhile(it => it != "Features").Skip(1).Take(1).FirstOrDefault(); - if (feature is not null) - { - _features[cacheKey] = feature; - return feature; - } - } - return null; - } - - /// - public void Apply(ControllerModel controller) => - controller.Properties.Add("feature", GetFeatureName(controller.ControllerType)); - -} - -/// -/// Expand the location token with the feature name -/// -public class FeatureViewLocationExpander : IViewLocationExpander -{ - /// - public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, - IEnumerable viewLocations) - { - _ = context ?? throw new ArgumentNullException(nameof(context)); - _ = viewLocations ?? throw new ArgumentNullException(nameof(viewLocations)); - if (context.ActionContext.ActionDescriptor is not ControllerActionDescriptor descriptor) - throw new ArgumentException("ActionDescriptor not found"); - - var feature = descriptor.Properties["feature"] as string ?? ""; - foreach (var location in viewLocations) - yield return location.Replace("{2}", feature); - } - - /// - public void PopulateValues(ViewLocationExpanderContext _) { } -} diff --git a/src/MyWebLog.CS/Features/Pages/All.cshtml b/src/MyWebLog.CS/Features/Pages/All.cshtml deleted file mode 100644 index 3d474fd..0000000 --- a/src/MyWebLog.CS/Features/Pages/All.cshtml +++ /dev/null @@ -1,37 +0,0 @@ -@model PageListModel -@{ - Layout = "_AdminLayout"; - ViewBag.Title = Resources.Pages; -} -
- @Resources.CreateANewPage - - - - - - - - - - - @foreach (var pg in Model.Pages) - { - - - - - - - } - -
@Resources.Actions@Resources.Title@Resources.InListQuestion@Resources.LastUpdated
- @Resources.Edit - - @pg.Title - @if (pg.Id == Model.WebLog.DefaultPage) - { -   HOME PAGE - } - @pg.UpdatedOn.ToString(Resources.DateFormatString)
-
diff --git a/src/MyWebLog.CS/Features/Pages/Edit.cshtml b/src/MyWebLog.CS/Features/Pages/Edit.cshtml deleted file mode 100644 index bd3482b..0000000 --- a/src/MyWebLog.CS/Features/Pages/Edit.cshtml +++ /dev/null @@ -1,56 +0,0 @@ -@model EditPageModel -@{ - Layout = "_AdminLayout"; - ViewBag.Title = Model.IsNew ? Resources.AddANewPage : Resources.EditPage; -} -
-

@ViewBag.Title

-
- -
-
-
-
- - - -
-
-
-
-
-
- - - -
-
-
-
- - -
-
-
-
-
-     - - - - -
-
-
-
- -
-
-
-
- -
-
-
-
-
diff --git a/src/MyWebLog.CS/Features/Pages/EditPageModel.cs b/src/MyWebLog.CS/Features/Pages/EditPageModel.cs deleted file mode 100644 index 5e0cd69..0000000 --- a/src/MyWebLog.CS/Features/Pages/EditPageModel.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace MyWebLog.Features.Pages; - -/// -/// Model used to edit pages -/// -public class EditPageModel : MyWebLogModel -{ - /// - /// The ID of the page being edited - /// - public string PageId { get; set; } = "new"; - - /// - /// Whether this is a new page - /// - public bool IsNew => PageId == "new"; - - /// - /// The title of the page - /// - [Display(ResourceType = typeof(Resources), Name = "Title")] - [Required(AllowEmptyStrings = false)] - public string Title { get; set; } = ""; - - /// - /// The permalink for the page - /// - [Display(ResourceType = typeof(Resources), Name = "Permalink")] - [Required(AllowEmptyStrings = false)] - public string Permalink { get; set; } = ""; - - /// - /// Whether this page is shown in the page list - /// - [Display(ResourceType = typeof(Resources), Name = "ShowInPageList")] - public bool IsShownInPageList { get; set; } = false; - - /// - /// The source format for the text - /// - public RevisionSource Source { get; set; } = RevisionSource.Html; - - /// - /// The text of the page - /// - [Display(ResourceType = typeof(Resources), Name = "PageText")] - [Required(AllowEmptyStrings = false)] - public string Text { get; set; } = ""; - - [Obsolete("Only used for model binding; use the WebLogDetails constructor")] - public EditPageModel() : base(new()) { } - - /// - public EditPageModel(WebLogDetails webLog) : base(webLog) { } - - /// - /// Create a model from an existing page - /// - /// The page from which the model will be created - /// The web log to which the page belongs - /// A populated model - public static EditPageModel CreateFromPage(Page page, WebLogDetails webLog) - { - var lastRev = page.Revisions.OrderByDescending(r => r.AsOf).First(); - return new(webLog) - { - PageId = page.Id, - Title = page.Title, - Permalink = page.Permalink, - IsShownInPageList = page.ShowInPageList, - Source = lastRev.SourceType, - Text = lastRev.Text - }; - } - - /// - /// Populate a page from the values contained in this page - /// - /// The page to be populated - /// The populated page - public Page? PopulatePage(Page? page) - { - if (page == null) return null; - - page.Title = Title; - page.Permalink = Permalink; - page.ShowInPageList = IsShownInPageList; - page.Revisions.Add(new() - { - Id = WebLogDbContext.NewId(), - AsOf = DateTime.UtcNow, - SourceType = Source, - Text = Text - }); - return page; - } -} diff --git a/src/MyWebLog.CS/Features/Pages/PageController.cs b/src/MyWebLog.CS/Features/Pages/PageController.cs deleted file mode 100644 index 9fc26d5..0000000 --- a/src/MyWebLog.CS/Features/Pages/PageController.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Markdig; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace MyWebLog.Features.Pages; - -/// -/// Handle routes for pages -/// -[Route("/page")] -[Authorize] -public class PageController : MyWebLogController -{ - /// - /// Pipeline with most extensions enabled - /// - private readonly MarkdownPipeline _pipeline = new MarkdownPipelineBuilder() - .UseSmartyPants().UseAdvancedExtensions().Build(); - - /// - public PageController(WebLogDbContext db) : base(db) { } - - [HttpGet("all")] - [HttpGet("all/page/{pageNbr:int}")] - public async Task All(int? pageNbr) => - View(new PageListModel(await Db.Pages.FindPageOfPages(pageNbr ?? 1), WebLog)); - - [HttpGet("{id}/edit")] - public async Task Edit(string id) - { - if (id == "new") return View(new EditPageModel(WebLog)); - - var page = await Db.Pages.FindByIdWithRevisions(id); - if (page == null) return NotFound(); - - return View(EditPageModel.CreateFromPage(page, WebLog)); - } - - [HttpPost("{id}/edit")] - public async Task Save(EditPageModel model) - { - var page = model.PopulatePage(model.IsNew - ? new() - { - Id = WebLogDbContext.NewId(), - AuthorId = UserId, - PublishedOn = DateTime.UtcNow, - Revisions = new List() - } - : await Db.Pages.GetById(model.PageId)); - if (page == null) return NotFound(); - - page.Text = model.Source == RevisionSource.Html ? model.Text : Markdown.ToHtml(model.Text, _pipeline); - page.UpdatedOn = DateTime.UtcNow; - - if (model.IsNew) await Db.Pages.AddAsync(page); - - await Db.SaveChangesAsync(); - - // TODO: confirmation - - return RedirectToAction(nameof(All)); - } -} diff --git a/src/MyWebLog.CS/Features/Pages/PageListModel.cs b/src/MyWebLog.CS/Features/Pages/PageListModel.cs deleted file mode 100644 index 056d233..0000000 --- a/src/MyWebLog.CS/Features/Pages/PageListModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MyWebLog.Features.Pages; - -/// -/// View model for viewing a list of pages -/// -public class PageListModel : MyWebLogModel -{ - public IList Pages { get; init; } - - /// - /// Constructor - /// - /// The pages to display - /// The web log details - public PageListModel(IList pages, WebLogDetails webLog) : base(webLog) - { - Pages = pages; - } -} diff --git a/src/MyWebLog.CS/Features/Pages/SinglePageModel.cs b/src/MyWebLog.CS/Features/Pages/SinglePageModel.cs deleted file mode 100644 index e9913c4..0000000 --- a/src/MyWebLog.CS/Features/Pages/SinglePageModel.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace MyWebLog.Features.Pages; - -/// -/// The model used to render a single page -/// -public class SinglePageModel : MyWebLogModel -{ - /// - /// The page to be rendered - /// - public Page Page { get; init; } - - /// - /// Is this the home page? - /// - public bool IsHome => Page.Id == WebLog.DefaultPage; - - /// - /// Constructor - /// - /// The page to be rendered - /// The details for the web log - public SinglePageModel(Page page, WebLogDetails webLog) : base(webLog) - { - Page = page; - } -} diff --git a/src/MyWebLog.CS/Features/Posts/MultiplePostModel.cs b/src/MyWebLog.CS/Features/Posts/MultiplePostModel.cs deleted file mode 100644 index 20c3fcb..0000000 --- a/src/MyWebLog.CS/Features/Posts/MultiplePostModel.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MyWebLog.Features.Posts; - -/// -/// The model used to render multiple posts -/// -public class MultiplePostModel : MyWebLogModel -{ - /// - /// The posts to be rendered - /// - public IEnumerable Posts { get; init; } - - /// - /// Constructor - /// - /// The posts to be rendered - /// The details for the web log - public MultiplePostModel(IEnumerable posts, WebLogDetails webLog) : base(webLog) - { - Posts = posts; - } -} diff --git a/src/MyWebLog.CS/Features/Posts/PostController.cs b/src/MyWebLog.CS/Features/Posts/PostController.cs deleted file mode 100644 index 91b34d7..0000000 --- a/src/MyWebLog.CS/Features/Posts/PostController.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MyWebLog.Features.Pages; - -namespace MyWebLog.Features.Posts; - -/// -/// Handle post-related requests -/// -[Route("/post")] -[Authorize] -public class PostController : MyWebLogController -{ - /// - public PostController(WebLogDbContext db) : base(db) { } - - [HttpGet("~/")] - [AllowAnonymous] - public async Task Index() - { - if (WebLog.DefaultPage == "posts") return await PageOfPosts(1); - - var page = await Db.Pages.FindById(WebLog.DefaultPage); - return page is null ? NotFound() : ThemedView(page.Template ?? "SinglePage", new SinglePageModel(page, WebLog)); - } - - [HttpGet("~/page/{pageNbr:int}")] - [AllowAnonymous] - public async Task PageOfPosts(int pageNbr) => - ThemedView("Index", - new MultiplePostModel(await Db.Posts.FindPageOfPublishedPosts(pageNbr, WebLog.PostsPerPage), WebLog)); - - [HttpGet("~/{*permalink}")] - public async Task CatchAll(string permalink) - { - var post = await Db.Posts.FindByPermalink(permalink); - if (post != null) - { - // TODO: return via single-post action - } - - var page = await Db.Pages.FindByPermalink(permalink); - if (page != null) - { - return ThemedView(page.Template ?? "SinglePage", new SinglePageModel(page, WebLog)); - } - - // TOOD: search prior permalinks for posts and pages - - // We tried, we really tried... - Console.Write($"Returning 404 for permalink |{permalink}|"); - return NotFound(); - } - - [HttpGet("all")] - public async Task All() - { - await Task.CompletedTask; - throw new NotImplementedException(); - } - - [HttpGet("{id}/edit")] - public async Task Edit(string id) - { - await Task.CompletedTask; - throw new NotImplementedException(); - } -} diff --git a/src/MyWebLog.CS/Features/Shared/MyWebLogController.cs b/src/MyWebLog.CS/Features/Shared/MyWebLogController.cs deleted file mode 100644 index b743564..0000000 --- a/src/MyWebLog.CS/Features/Shared/MyWebLogController.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; - -namespace MyWebLog.Features.Shared; - -/// -/// Base class for myWebLog controllers -/// -public abstract class MyWebLogController : Controller -{ - /// - /// The data context to use to fulfil this request - /// - protected WebLogDbContext Db { get; init; } - - /// - /// The details for the current web log - /// - protected WebLogDetails WebLog => WebLogCache.Get(HttpContext); - - /// - /// The ID of the currently authenticated user - /// - protected string UserId => User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? ""; - - /// - /// Constructor - /// - /// The data context to use to fulfil this request - protected MyWebLogController(WebLogDbContext db) : base() - { - Db = db; - } - - protected ViewResult ThemedView(string template, object model) - { - // TODO: get actual version - ViewBag.Version = "2"; - return View(template, model); - } -} diff --git a/src/MyWebLog.CS/Features/Shared/MyWebLogModel.cs b/src/MyWebLog.CS/Features/Shared/MyWebLogModel.cs deleted file mode 100644 index 48d6e7f..0000000 --- a/src/MyWebLog.CS/Features/Shared/MyWebLogModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MyWebLog.Features.Shared; - -/// -/// Base model class for myWebLog views -/// -public class MyWebLogModel -{ - /// - /// The details for the web log - /// - public WebLogDetails WebLog { get; init; } - - /// - /// Constructor - /// - /// The details for the web log - protected MyWebLogModel(WebLogDetails webLog) - { - WebLog = webLog; - } -} diff --git a/src/MyWebLog.CS/Features/Shared/TagHelpers/ImageTagHelper.cs b/src/MyWebLog.CS/Features/Shared/TagHelpers/ImageTagHelper.cs deleted file mode 100644 index c1ae230..0000000 --- a/src/MyWebLog.CS/Features/Shared/TagHelpers/ImageTagHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Razor.TagHelpers; -using System.Text.Encodings.Web; - -namespace MyWebLog.Features.Shared.TagHelpers; - -/// -/// Image tag helper to load a theme's image -/// -[HtmlTargetElement("img", Attributes = "asp-theme")] -public class ImageTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.ImageTagHelper -{ - /// - /// The theme for which the image should be loaded - /// - [HtmlAttributeName("asp-theme")] - public string Theme { get; set; } = ""; - - /// - public ImageTagHelper(IFileVersionProvider fileVersionProvider, HtmlEncoder htmlEncoder, - IUrlHelperFactory urlHelperFactory) - : base(fileVersionProvider, htmlEncoder, urlHelperFactory) { } - - /// - public override void Process(TagHelperContext context, TagHelperOutput output) - { - if (Theme == "") - { - base.Process(context, output); - return; - } - - output.Attributes.SetAttribute("src", $"~/{Theme}/img/{context.AllAttributes["src"]?.Value}"); - ProcessUrlAttribute("src", output); - } -} diff --git a/src/MyWebLog.CS/Features/Shared/TagHelpers/LinkTagHelper.cs b/src/MyWebLog.CS/Features/Shared/TagHelpers/LinkTagHelper.cs deleted file mode 100644 index 0889f24..0000000 --- a/src/MyWebLog.CS/Features/Shared/TagHelpers/LinkTagHelper.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Razor.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Razor.TagHelpers; -using System.Text.Encodings.Web; - -namespace MyWebLog.Features.Shared.TagHelpers; - -/// -/// Tag helper to link stylesheets for a theme -/// -[HtmlTargetElement("link", Attributes = "asp-theme")] -public class LinkTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.LinkTagHelper -{ - /// - /// The theme for which a style sheet should be loaded - /// - [HtmlAttributeName("asp-theme")] - public string Theme { get; set; } = ""; - - /// - /// The style sheet to be loaded (defaults to "style") - /// - [HtmlAttributeName("asp-style")] - public string Style { get; set; } = "style"; - - /// - public LinkTagHelper(IWebHostEnvironment hostingEnvironment, TagHelperMemoryCacheProvider cacheProvider, - IFileVersionProvider fileVersionProvider, HtmlEncoder htmlEncoder, JavaScriptEncoder javaScriptEncoder, - IUrlHelperFactory urlHelperFactory) - : base(hostingEnvironment, cacheProvider, fileVersionProvider, htmlEncoder, javaScriptEncoder, urlHelperFactory) - { } - - /// - public override void Process(TagHelperContext context, TagHelperOutput output) - { - if (Theme == "") - { - base.Process(context, output); - return; - } - - switch (context.AllAttributes["rel"]?.Value.ToString()) - { - case "stylesheet": - output.Attributes.SetAttribute("href", $"~/{Theme}/css/{Style}.css"); - break; - case "icon": - output.Attributes.SetAttribute("type", "image/x-icon"); - output.Attributes.SetAttribute("href", $"~/{Theme}/img/favicon.ico"); - break; - } - ProcessUrlAttribute("href", output); - } -} diff --git a/src/MyWebLog.CS/Features/Shared/TagHelpers/YesNoTagHelper.cs b/src/MyWebLog.CS/Features/Shared/TagHelpers/YesNoTagHelper.cs deleted file mode 100644 index 945c561..0000000 --- a/src/MyWebLog.CS/Features/Shared/TagHelpers/YesNoTagHelper.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Razor.TagHelpers; - -namespace MyWebLog.Features.Shared.TagHelpers; - -/// -/// Write a Yes or No based on a boolean value -/// -public class YesNoTagHelper : TagHelper -{ - /// - /// The attribute in question - /// - [HtmlAttributeName("asp-for")] - public bool For { get; set; } = false; - - /// - /// Optional; if set, that value will be wrapped with <strong> instead of <span> - /// - [HtmlAttributeName("asp-strong-if")] - public bool? StrongIf { get; set; } - - /// - public override void Process(TagHelperContext context, TagHelperOutput output) - { - output.TagName = For == StrongIf ? "strong" : "span"; - output.TagMode = TagMode.StartTagAndEndTag; - output.Content.Append(For ? Resources.Yes : Resources.No); - } -} diff --git a/src/MyWebLog.CS/Features/Shared/_AdminLayout.cshtml b/src/MyWebLog.CS/Features/Shared/_AdminLayout.cshtml deleted file mode 100644 index 3b62f66..0000000 --- a/src/MyWebLog.CS/Features/Shared/_AdminLayout.cshtml +++ /dev/null @@ -1,44 +0,0 @@ -@model MyWebLogModel - - - - - @ViewBag.Title « @Resources.Admin « @Model.WebLog.Name - - - - -
- -
-
- @* Each.Messages - @Current.ToDisplay - @EndEach *@ - @RenderBody() -
-
-
-
-
myWebLog
-
-
-
- - - diff --git a/src/MyWebLog.CS/Features/Shared/_LogOnOffPartial.cshtml b/src/MyWebLog.CS/Features/Shared/_LogOnOffPartial.cshtml deleted file mode 100644 index c182a99..0000000 --- a/src/MyWebLog.CS/Features/Shared/_LogOnOffPartial.cshtml +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/src/MyWebLog.CS/Features/ThemeSupport.cs b/src/MyWebLog.CS/Features/ThemeSupport.cs deleted file mode 100644 index 1e495e8..0000000 --- a/src/MyWebLog.CS/Features/ThemeSupport.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Razor; - -namespace MyWebLog.Features; - -/// -/// Expand the location token with the theme path -/// -public class ThemeViewLocationExpander : IViewLocationExpander -{ - /// - public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, - IEnumerable viewLocations) - { - _ = context ?? throw new ArgumentNullException(nameof(context)); - _ = viewLocations ?? throw new ArgumentNullException(nameof(viewLocations)); - - foreach (var location in viewLocations) - yield return location.Replace("{3}", context.Values["theme"]!); - } - - /// - public void PopulateValues(ViewLocationExpanderContext context) - { - _ = context ?? throw new ArgumentNullException(nameof(context)); - - context.Values["theme"] = WebLogCache.Get(context.ActionContext.HttpContext).ThemePath; - } -} diff --git a/src/MyWebLog.CS/Features/Users/LogOn.cshtml b/src/MyWebLog.CS/Features/Users/LogOn.cshtml deleted file mode 100644 index 9c77857..0000000 --- a/src/MyWebLog.CS/Features/Users/LogOn.cshtml +++ /dev/null @@ -1,31 +0,0 @@ -@model LogOnModel -@{ - Layout = "_AdminLayout"; - ViewBag.Title = @Resources.LogOn; -} -

@Resources.LogOnTo @Model.WebLog.Name

-
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
-
- -
-
-
-
-
diff --git a/src/MyWebLog.CS/Features/Users/LogOnModel.cs b/src/MyWebLog.CS/Features/Users/LogOnModel.cs deleted file mode 100644 index 347665b..0000000 --- a/src/MyWebLog.CS/Features/Users/LogOnModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace MyWebLog.Features.Users; - -/// -/// The model to use to allow a user to log on -/// -public class LogOnModel : MyWebLogModel -{ - /// - /// The user's e-mail address - /// - [Required(AllowEmptyStrings = false)] - [EmailAddress] - [Display(ResourceType = typeof(Resources), Name = "EmailAddress")] - public string EmailAddress { get; set; } = ""; - - /// - /// The user's password - /// - [Required(AllowEmptyStrings = false)] - [Display(ResourceType = typeof(Resources), Name = "Password")] - public string Password { get; set; } = ""; - - [Obsolete("Only used for model binding; use the WebLogDetails constructor")] - public LogOnModel() : base(new()) { } - - /// - public LogOnModel(WebLogDetails webLog) : base(webLog) { } -} diff --git a/src/MyWebLog.CS/Features/Users/UserController.cs b/src/MyWebLog.CS/Features/Users/UserController.cs deleted file mode 100644 index bb377ea..0000000 --- a/src/MyWebLog.CS/Features/Users/UserController.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; - -namespace MyWebLog.Features.Users; - -/// -/// Controller for the users feature -/// -[Route("/user")] -public class UserController : MyWebLogController -{ - /// - /// Hash a password for a given user - /// - /// The plain-text password - /// The user's e-mail address - /// The user-specific salt - /// - internal static string HashedPassword(string plainText, string email, Guid salt) - { - var allSalt = salt.ToByteArray().Concat(Encoding.UTF8.GetBytes(email)).ToArray(); - using Rfc2898DeriveBytes alg = new(plainText, allSalt, 2_048); - return Convert.ToBase64String(alg.GetBytes(64)); - } - - /// - public UserController(WebLogDbContext db) : base(db) { } - - [HttpGet("log-on")] - public IActionResult LogOn() => - View(new LogOnModel(WebLog)); - - [HttpPost("log-on")] - public async Task DoLogOn(LogOnModel model) - { - var user = await Db.Users.FindByEmail(model.EmailAddress); - - if (user == null || user.PasswordHash != HashedPassword(model.Password, user.UserName, user.Salt)) - { - // TODO: make error, not 404 - return NotFound(); - } - - List claims = new() - { - new(ClaimTypes.NameIdentifier, user.Id), - new(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"), - new(ClaimTypes.GivenName, user.PreferredName), - new(ClaimTypes.Role, user.AuthorizationLevel.ToString()) - }; - ClaimsIdentity identity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme); - - await HttpContext.SignInAsync(identity.AuthenticationType, new(identity), - new() { IssuedUtc = DateTime.UtcNow }); - - // TODO: confirmation message - - return RedirectToAction("Index", "Admin"); - } - - [HttpGet("log-off")] - [Authorize] - public async Task LogOff() - { - await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - - // TODO: confirmation message - - return LocalRedirect("~/"); - } -} diff --git a/src/MyWebLog.CS/Features/_ViewImports.cshtml b/src/MyWebLog.CS/Features/_ViewImports.cshtml deleted file mode 100644 index 5538471..0000000 --- a/src/MyWebLog.CS/Features/_ViewImports.cshtml +++ /dev/null @@ -1,7 +0,0 @@ -@namespace MyWebLog.Features - -@using MyWebLog -@using MyWebLog.Properties - -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@addTagHelper *, MyWebLog diff --git a/src/MyWebLog.CS/GlobalUsings.cs b/src/MyWebLog.CS/GlobalUsings.cs deleted file mode 100644 index 45f4200..0000000 --- a/src/MyWebLog.CS/GlobalUsings.cs +++ /dev/null @@ -1,3 +0,0 @@ -global using MyWebLog.Data; -global using MyWebLog.Features.Shared; -global using MyWebLog.Properties; diff --git a/src/MyWebLog.CS/MyWebLog.CS.csproj b/src/MyWebLog.CS/MyWebLog.CS.csproj deleted file mode 100644 index b1c1626..0000000 --- a/src/MyWebLog.CS/MyWebLog.CS.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - True - True - Resources.resx - - - - - - PublicResXFileCodeGenerator - Resources.Designer.cs - - - - diff --git a/src/MyWebLog.CS/Program.cs b/src/MyWebLog.CS/Program.cs deleted file mode 100644 index 03c9b0e..0000000 --- a/src/MyWebLog.CS/Program.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using MyWebLog; -using MyWebLog.Features; -using MyWebLog.Features.Users; -using System.Reflection; - -if (args.Length > 0 && args[0] == "init") -{ - await InitDb(); - return; -} - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddMvc(opts => -{ - opts.Conventions.Add(new FeatureControllerModelConvention()); - opts.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); -}).AddRazorOptions(opts => -{ - opts.ViewLocationFormats.Clear(); - opts.ViewLocationFormats.Add("/Themes/{3}/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Themes/{3}/Shared/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Themes/Default/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Themes/Default/Shared/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Features/{2}/{1}/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Features/{2}/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Features/Shared/{0}.cshtml"); - opts.ViewLocationExpanders.Add(new FeatureViewLocationExpander()); - opts.ViewLocationExpanders.Add(new ThemeViewLocationExpander()); -}); -builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) - .AddCookie(opts => - { - opts.ExpireTimeSpan = TimeSpan.FromMinutes(20); - opts.SlidingExpiration = true; - opts.AccessDeniedPath = "/forbidden"; - }); -builder.Services.AddAuthorization(); -builder.Services.AddSingleton(); -builder.Services.AddDbContext(o => -{ - // TODO: can get from DI? - var db = WebLogCache.HostToDb(new HttpContextAccessor().HttpContext!); - // "empty"; - o.UseSqlite($"Data Source=Db/{db}.db"); -}); - -// Load themes -Array.ForEach(Directory.GetFiles(Directory.GetCurrentDirectory(), "MyWebLog.Themes.*.dll"), - it => { Assembly.LoadFile(it); }); - -var app = builder.Build(); - -app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Strict }); -app.UseMiddleware(); -app.UseAuthentication(); -app.UseStaticFiles(); -app.UseRouting(); -app.UseAuthorization(); -app.UseEndpoints(endpoints => endpoints.MapControllers()); - -app.Run(); - -/// -/// Initialize a new database -/// -async Task InitDb() -{ - if (args.Length != 5) - { - Console.WriteLine("Usage: MyWebLog init [url] [name] [admin-email] [admin-pw]"); - return; - } - - using var db = new WebLogDbContext(new DbContextOptionsBuilder() - .UseSqlite($"Data Source=Db/{args[1].Replace(':', '_')}.db").Options); - await db.Database.MigrateAsync(); - - // Create the admin user - var salt = Guid.NewGuid(); - var user = new WebLogUser - { - Id = WebLogDbContext.NewId(), - UserName = args[3], - FirstName = "Admin", - LastName = "User", - PreferredName = "Admin", - PasswordHash = UserController.HashedPassword(args[4], args[3], salt), - Salt = salt, - AuthorizationLevel = AuthorizationLevel.Administrator - }; - await db.Users.AddAsync(user); - - // Create the default home page - var home = new Page - { - Id = WebLogDbContext.NewId(), - AuthorId = user.Id, - Title = "Welcome to myWebLog!", - Permalink = "welcome-to-myweblog.html", - PublishedOn = DateTime.UtcNow, - UpdatedOn = DateTime.UtcNow, - Text = "

This is your default home page.

", - Revisions = new[] - { - new PageRevision - { - Id = WebLogDbContext.NewId(), - AsOf = DateTime.UtcNow, - SourceType = RevisionSource.Html, - Text = "

This is your default home page.

" - } - } - }; - await db.Pages.AddAsync(home); - - // Add the details - var timeZone = TimeZoneInfo.Local.Id; - if (!TimeZoneInfo.Local.HasIanaId) - { - timeZone = TimeZoneInfo.TryConvertWindowsIdToIanaId(timeZone, out var ianaId) - ? ianaId - : throw new TimeZoneNotFoundException($"Cannot find IANA timezone for {timeZone}"); - } - var details = new WebLogDetails - { - Name = args[2], - UrlBase = args[1], - DefaultPage = home.Id, - TimeZone = timeZone - }; - await db.WebLogDetails.AddAsync(details); - - await db.SaveChangesAsync(); - - Console.WriteLine($"Successfully initialized database for {args[2]} with URL base {args[1]}"); -} diff --git a/src/MyWebLog.CS/Properties/Resources.Designer.cs b/src/MyWebLog.CS/Properties/Resources.Designer.cs deleted file mode 100644 index 7046e4e..0000000 --- a/src/MyWebLog.CS/Properties/Resources.Designer.cs +++ /dev/null @@ -1,459 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace MyWebLog.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MyWebLog.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Actions. - /// - public static string Actions { - get { - return ResourceManager.GetString("Actions", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add a New Category. - /// - public static string AddANewCategory { - get { - return ResourceManager.GetString("AddANewCategory", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add a New Page. - /// - public static string AddANewPage { - get { - return ResourceManager.GetString("AddANewPage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Admin. - /// - public static string Admin { - get { - return ResourceManager.GetString("Admin", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All. - /// - public static string All { - get { - return ResourceManager.GetString("All", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Categories. - /// - public static string Categories { - get { - return ResourceManager.GetString("Categories", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Create a New Page. - /// - public static string CreateANewPage { - get { - return ResourceManager.GetString("CreateANewPage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dashboard. - /// - public static string Dashboard { - get { - return ResourceManager.GetString("Dashboard", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MMMM d, yyyy. - /// - public static string DateFormatString { - get { - return ResourceManager.GetString("DateFormatString", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Default Page. - /// - public static string DefaultPage { - get { - return ResourceManager.GetString("DefaultPage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Drafts. - /// - public static string Drafts { - get { - return ResourceManager.GetString("Drafts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edit. - /// - public static string Edit { - get { - return ResourceManager.GetString("Edit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edit Page. - /// - public static string EditPage { - get { - return ResourceManager.GetString("EditPage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to E-mail Address. - /// - public static string EmailAddress { - get { - return ResourceManager.GetString("EmailAddress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to First Page of Posts. - /// - public static string FirstPageOfPosts { - get { - return ResourceManager.GetString("FirstPageOfPosts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to In List?. - /// - public static string InListQuestion { - get { - return ResourceManager.GetString("InListQuestion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Last Updated. - /// - public static string LastUpdated { - get { - return ResourceManager.GetString("LastUpdated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Log Off. - /// - public static string LogOff { - get { - return ResourceManager.GetString("LogOff", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Log On. - /// - public static string LogOn { - get { - return ResourceManager.GetString("LogOn", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Log On to. - /// - public static string LogOnTo { - get { - return ResourceManager.GetString("LogOnTo", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Modify Settings. - /// - public static string ModifySettings { - get { - return ResourceManager.GetString("ModifySettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Name. - /// - public static string Name { - get { - return ResourceManager.GetString("Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No. - /// - public static string No { - get { - return ResourceManager.GetString("No", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Pages. - /// - public static string Pages { - get { - return ResourceManager.GetString("Pages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Page Text. - /// - public static string PageText { - get { - return ResourceManager.GetString("PageText", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Password. - /// - public static string Password { - get { - return ResourceManager.GetString("Password", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Permalink. - /// - public static string Permalink { - get { - return ResourceManager.GetString("Permalink", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Posts. - /// - public static string Posts { - get { - return ResourceManager.GetString("Posts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Posts per Page. - /// - public static string PostsPerPage { - get { - return ResourceManager.GetString("PostsPerPage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Published. - /// - public static string Published { - get { - return ResourceManager.GetString("Published", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Save Changes. - /// - public static string SaveChanges { - get { - return ResourceManager.GetString("SaveChanges", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show in Page List. - /// - public static string ShowInPageList { - get { - return ResourceManager.GetString("ShowInPageList", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Shown in Page List. - /// - public static string ShownInPageList { - get { - return ResourceManager.GetString("ShownInPageList", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Subtitle. - /// - public static string Subtitle { - get { - return ResourceManager.GetString("Subtitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are {0} categories. - /// - public static string ThereAreXCategories { - get { - return ResourceManager.GetString("ThereAreXCategories", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are {0} pages. - /// - public static string ThereAreXPages { - get { - return ResourceManager.GetString("ThereAreXPages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are {0} published posts and {1} drafts. - /// - public static string ThereAreXPublishedPostsAndYDrafts { - get { - return ResourceManager.GetString("ThereAreXPublishedPostsAndYDrafts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Time Zone. - /// - public static string TimeZone { - get { - return ResourceManager.GetString("TimeZone", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Title. - /// - public static string Title { - get { - return ResourceManager.GetString("Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Top Level. - /// - public static string TopLevel { - get { - return ResourceManager.GetString("TopLevel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to View All. - /// - public static string ViewAll { - get { - return ResourceManager.GetString("ViewAll", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Web Log Settings. - /// - public static string WebLogSettings { - get { - return ResourceManager.GetString("WebLogSettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Write a New Post. - /// - public static string WriteANewPost { - get { - return ResourceManager.GetString("WriteANewPost", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Yes. - /// - public static string Yes { - get { - return ResourceManager.GetString("Yes", resourceCulture); - } - } - } -} diff --git a/src/MyWebLog.CS/Properties/Resources.resx b/src/MyWebLog.CS/Properties/Resources.resx deleted file mode 100644 index 1b52285..0000000 --- a/src/MyWebLog.CS/Properties/Resources.resx +++ /dev/null @@ -1,252 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Actions - - - Add a New Category - - - Add a New Page - - - Admin - - - All - - - Categories - - - Create a New Page - - - Dashboard - - - MMMM d, yyyy - - - Default Page - - - Drafts - - - Edit - - - Edit Page - - - E-mail Address - - - First Page of Posts - - - In List? - - - Last Updated - - - Log Off - - - Log On - - - Log On to - - - Modify Settings - - - Name - - - No - - - Pages - - - Page Text - - - Password - - - Permalink - - - Posts - - - Posts per Page - - - Published - - - Save Changes - - - Show in Page List - - - Shown in Page List - - - Subtitle - - - There are {0} categories - - - There are {0} pages - - - There are {0} published posts and {1} drafts - - - Time Zone - - - Title - - - Top Level - - - View All - - - Web Log Settings - - - Write a New Post - - - Yes - - \ No newline at end of file diff --git a/src/MyWebLog.CS/Properties/launchSettings.json b/src/MyWebLog.CS/Properties/launchSettings.json deleted file mode 100644 index 7d7face..0000000 --- a/src/MyWebLog.CS/Properties/launchSettings.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:3330", - "sslPort": 0 - } - }, - "profiles": { - "MyWebLog": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:5010", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/src/MyWebLog.CS/Themes/Default/Shared/_DefaultFooter.cshtml b/src/MyWebLog.CS/Themes/Default/Shared/_DefaultFooter.cshtml deleted file mode 100644 index d32c8fa..0000000 --- a/src/MyWebLog.CS/Themes/Default/Shared/_DefaultFooter.cshtml +++ /dev/null @@ -1,6 +0,0 @@ -
-
-
- myWebLog -
-
diff --git a/src/MyWebLog.CS/Themes/Default/Shared/_DefaultHeader.cshtml b/src/MyWebLog.CS/Themes/Default/Shared/_DefaultHeader.cshtml deleted file mode 100644 index 794ce42..0000000 --- a/src/MyWebLog.CS/Themes/Default/Shared/_DefaultHeader.cshtml +++ /dev/null @@ -1,20 +0,0 @@ -@model MyWebLogModel -
- -
diff --git a/src/MyWebLog.CS/Themes/Default/Shared/_Layout.cshtml b/src/MyWebLog.CS/Themes/Default/Shared/_Layout.cshtml deleted file mode 100644 index 37a8fbd..0000000 --- a/src/MyWebLog.CS/Themes/Default/Shared/_Layout.cshtml +++ /dev/null @@ -1,35 +0,0 @@ -@model MyWebLogModel - - - - - - - - - @await RenderSectionAsync("Style", false) - @ViewBag.Title « @Model.WebLog.Name - - - @if (IsSectionDefined("Header")) - { - @await RenderSectionAsync("Header") - } - else - { - @await Html.PartialAsync("_DefaultHeader", Model) - } -
- @RenderBody() -
- @if (IsSectionDefined("Footer")) - { - @await RenderSectionAsync("Footer") - } else - { - @await Html.PartialAsync("_DefaultFooter") - } - @await RenderSectionAsync("Script", false) - - diff --git a/src/MyWebLog.CS/Themes/Default/SinglePage.cshtml b/src/MyWebLog.CS/Themes/Default/SinglePage.cshtml deleted file mode 100644 index adde186..0000000 --- a/src/MyWebLog.CS/Themes/Default/SinglePage.cshtml +++ /dev/null @@ -1,10 +0,0 @@ -@using MyWebLog.Features.Pages -@model SinglePageModel -@{ - Layout = "_Layout"; - ViewBag.Title = Model.Page.Title; -} -

@Model.Page.Title

-
- @Html.Raw(Model.Page.Text) -
diff --git a/src/MyWebLog.CS/Themes/_ViewImports.cshtml b/src/MyWebLog.CS/Themes/_ViewImports.cshtml deleted file mode 100644 index 16800cf..0000000 --- a/src/MyWebLog.CS/Themes/_ViewImports.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@namespace MyWebLog.Themes - -@addTagHelper *, MyWebLog diff --git a/src/MyWebLog.CS/WebLogMiddleware.cs b/src/MyWebLog.CS/WebLogMiddleware.cs deleted file mode 100644 index bc24d0e..0000000 --- a/src/MyWebLog.CS/WebLogMiddleware.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Concurrent; - -namespace MyWebLog; - -/// -/// In-memory cache of web log details -/// -/// This is filled by the middleware via the first request for each host, and can be updated via the web log -/// settings update page -public static class WebLogCache -{ - /// - /// The cache of web log details - /// - private static readonly ConcurrentDictionary _cache = new(); - - /// - /// Transform a hostname to a database name - /// - /// The current HTTP context - /// The hostname, with an underscore replacing a colon - public static string HostToDb(HttpContext ctx) => ctx.Request.Host.ToUriComponent().Replace(':', '_'); - - /// - /// Does a host exist in the cache? - /// - /// The host in question - /// True if it exists, false if not - public static bool Exists(string host) => _cache.ContainsKey(host); - - /// - /// Get the details for a web log via its host - /// - /// The host which should be retrieved - /// The web log details - public static WebLogDetails Get(string host) => _cache[host]; - - /// - /// Get the details for a web log via its host - /// - /// The HTTP context for the request - /// The web log details - public static WebLogDetails Get(HttpContext ctx) => _cache[HostToDb(ctx)]; - - /// - /// Set the details for a particular host - /// - /// The host for which details should be set - /// The details to be set - public static void Set(string host, WebLogDetails details) => _cache[host] = details; -} - -/// -/// Middleware to derive the current web log -/// -public class WebLogMiddleware -{ - /// - /// The next action in the pipeline - /// - private readonly RequestDelegate _next; - - /// - /// Constructor - /// - /// The next action in the pipeline - public WebLogMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task InvokeAsync(HttpContext context) - { - var host = WebLogCache.HostToDb(context); - - if (!WebLogCache.Exists(host)) - { - var db = context.RequestServices.GetRequiredService(); - var details = await db.WebLogDetails.FindByHost(context.Request.Host.ToUriComponent()); - if (details == null) - { - context.Response.StatusCode = 404; - return; - } - - WebLogCache.Set(host, details); - } - - await _next.Invoke(context); - } -} diff --git a/src/MyWebLog.CS/appsettings.Development.json b/src/MyWebLog.CS/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/src/MyWebLog.CS/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/src/MyWebLog.CS/appsettings.json b/src/MyWebLog.CS/appsettings.json deleted file mode 100644 index 10f68b8..0000000 --- a/src/MyWebLog.CS/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/src/MyWebLog.CS/wwwroot/css/admin.css b/src/MyWebLog.CS/wwwroot/css/admin.css deleted file mode 100644 index f720fda..0000000 --- a/src/MyWebLog.CS/wwwroot/css/admin.css +++ /dev/null @@ -1,5 +0,0 @@ -footer { - background-color: #808080; - border-top: solid 1px black; - color: white; -} diff --git a/src/MyWebLog.CS/wwwroot/img/logo-dark.png b/src/MyWebLog.CS/wwwroot/img/logo-dark.png deleted file mode 100644 index 19bdcca2af3095d7f18345a28fbf9c6da0363daa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3362 zcmV+-4c+pIP)WFU8GbZ8()Nlj2>E@cM*01SpnL_t(|+U=ZskQCJw z#(zC5Ao3EC2oXWEf}*0J5)f1nK`94}j&Xi*#JudGcqa6%S+KT&-RP!O=+F+ip2H>e(2QR}+oFd72i zs+)9OzW2^kV49E*e<~SAs|Fbs5 zlDRJY=7E3{pwAWZ_d8&@lv@B7W=ZA+mO>V_i4(+MjxV9zGPG|7?3K2QfIC#*do%QH zo$|jSiV9{9}K5=-W~bTu*&Qv1s<1MdPm z<$HIaO+XhTlO)#$xE**4*aqwaz6O@y>eMX`VxFe}(||QV1+WYF7p}&B!V$Rc?PO(J z0&I~LZzr%8cm(Km5M%xXm?MUw64(wb1;UE2NA(KRRZVao&^%4sFQwfU1vXX__Wc%-Vk;C2kPQ~?W2eksJ`=j#viWQY>7kRh+E)=K5{MNZKtLg^KR8{3(&)(O)KD^DG$*hk~zXv zL?|Xo8Bs>(5Sa(7NpAFM^N!T-2U@1otx)~X_vl-y>TmL>Z=!DAHsFUD&bK(jN}_LC zS%&f2h}#lx-?NU$+)AwkRb-Yjdq_KjD}%Q78M9K|mvhsMcdYp10p}5>4-sY(Wi?T@ zGD7^Fp&TQ}jrzn0)A^8T7^X8378B(g!nE^kS3naY+)soGA}nMmO;v4US^nb_?UY8s zTav*#-0l%5m#O-W87%jh0JH9)+CQ3s%o75PH$}C-ua3xkvl`=xfPH4GKEL#+AFJwT z1Q_pHzj=fdp_&n8Dh^9=7%9G2Tk<%aDA&@G&*k_&43&u`GnC{zI)s@-gfEEjmq39; zm?$CM$DuE9JjQ2xI6^n7S&hR)9Gc-Ui01TGE%u7k=BH{5R`c~zBJ{CA9=>+ms;mVz z`$XY!!aNgT?rT*!lQ#ZrfbsPCpI=9--({*iD`20$tMWx2_5Dw8MWX-$MBgm@yM zJ{Ue=W{P$@l!k(a7 z9qeLF%CUdQMTtN0jT4XL0c-mOu-fCS;>v@2=E<6JkS**shd`1hY3eEhjgf zFaF>ia>ABN<|5H1-GHhTM=XWm6C56+n7^j)BxJrKfSZ6w zKQURc;+Fhk4y1X$E)lmEb^>js?SnE-Z6N3_Z3k7}UPol!A#-dc+^NdKnxot}QXagP z1m6*D9_&K3pg2j(Gz_P(29d!e`FM?Rm`bsb%P1ElCh3)<=t7KzIE)8YQp7+UvEFW< zH;A#42xBp<6e3RtE~khQl(L6*T&^Z!sNiv*InDR*o8$iSzLX#_72B0B*b(SL@Ga~O z%0Sd+0Cv2x`swgvRbE*~WY%-+9`jVlcQH9pkExhP9j`Nmo#P-Ebd0PVDExTKEAyyzh2 zI82qj42cfQWnTCjv|WE47eh()Q1>DX<|t=7~VjK$r}rgXYkRq1sxaAvU(A=H*lVmS7*>?OO$Idnqaxn zL2*w8>-wt49PlUfIAQ%xyT_jKeCbxt^K++F&3=cviVUcT>n+0V4ZvNKOXYMDS$#*}(NF$NB|W zzMDuJom=o9xe1$vWUYX+B=~Jp<37|>0lvgd!Wo9UO+Hs1oaR7Zk~8Jz0z(5R3zb<| z?;#$F#w#iA*K*vv@G{^yncJmmUej>XUj1+(uvp2j6`~}4B>%6c1o0u~kC0E4o`kuDFp~*$4gEZq-~}XaQaqByf4wfH?=FJ99~gg-W|N#XmhSUn z8sf#bY+DFf(v4HBq%$&%na#(~k1q@`#|mJ4!0R?h6rpsROhbgw@nfHP>CCwc!;J}l z(O_^`hQScxE8Lx~w&XL9#Wj+3=99b&n2jH)B|3mtvsk+KNHF$iV5C^j4kABQ%8K!^ zNRx9(-q!X*Ic^$gM8-Tr2(?uL&mswK4;S_VQIR0S5-7J9H)w~r4GV?v+p@Ukoq=CV z3~;It_H!{z(`9u%PWAcL+s(*DL^%(`avZTZF@8&oXDN}~hywD6u?jeoU9_as-=DHL zYS2|le6MMrJ$GN53NsnQiUg5i6~#-%I7FC= z!^D*1lRbU75*AT+i8U6vXyy)JY3{li{b)cwvn2;(KJYv7mEWW&(8htq(T%ad!&2T1 zN)Ba^ zBTMFyoJfd!sV2%y${58Z=sZ~y_&1X+ERL|6k;fk~tYaoG0MLLbsC96J{-PcW8%j|^lu~Ios|1*VUfdRc7UsiF^k1i z6JZoi;x`$dqcG(s2P`ZClIrkd6kTY*TB_-dVLN$z1T?}>iJ?EmNk1NLVUfd@xq%4e zA=;A1T;gvSINV{0Y*D`~^H7eV1@GfWFU8GbZ8()Nlj2>E@cM*01t~vL_t(|+U=ZsbXC=z z$3Oern-~HFP@qszX~72yB0h==Dk_4nSuQ%l@?FWjI`uJjX)EoFikVWzQk>3=Gu75Y zfGJ`PKI&t@I>^I*|&)&b^ zIeYK#`R(8S?ccc&AV7cs0RjXF5a8)^**BEZNPS zk=0tyuw=IcFGxV<1A!N;|6kmxjyuj}>HjOGPH1m$KgO@DQtBeBxGb=V0htekWwyzD zYAhD(-wE-{>_Smd(PTeDpM3I32{7JzhJefg_8&DhHAN!Ql3qTw*4OxvOC%D~pkiI&VRkK6}^OAz?s-QPM_S(CKLr_Xz*B~$PHZLO}Z4*R&@&n?*(4krIL@R_^+ zrp6}Jq>73P<-_N_)RWx}yzX5q8$EjTSw3z#(Yp8Yz?=j)V2H(H{Y6BltxhtTJSAWI z#>U28B9ibd)GDp@y_UtAUEp3v*!OC0Z~s{t}pzfXrRyYAB`h64DOIX|0#7Tet2FW6Tck z(wKNWJ|K^KGSMpLjbg;(@c~-vdqiZNh-?#)q=2|K`UX*8pd6S5=nmiDZ?XK%y;MXN^UH3{6*_B6Mlk2+Y|1I2%ii!%QwZ2b8+Drk6 z$m0zS4W)&2HZ?W%7Ln%*=vrQ1U*F@9kIINhokg@Oa*%$EH4v2+`EIbTFV0~VziqXN z*VWY>mg6~oX0;O$c}YYnwAOcu$S*`>qy6HgQmL_pi08WQBu`gTM4r)F-=np@TSOL$ z$WA*{m`o-|b~TwrJ{z}Uw7?>V>5&1k@L_}+Sv!(yJ9Qv=e z+I)~qCdcHkYIj-fYSYVS4&t?l$W{^AoJb^&&$iTy$eULCSG$tTTI(NKdYWC=y(C+^ zR4O%2L_W9L&dD=(Kea=psZ{EW%r>p9t^Gvgj~Oq~ev&!DMmqtY^u^=x0V1;9dj7FpL1vFoJ3Sq4DwWC`%1otFXNZXFwcj0!#g53Kvn@R@$z*bL zE`1FR4W%No$$vk}8ApT#M0uYmTe+S+{?1fNyz9pLCzNyCelxM0vJ1^*wf7RaKQEB8^r^?wEX* zTWftwp)RLVsWYrec_bg1wbpm}SeYs;eGha6nYGqGuyp*^=Zd-5>c_2Fblzs^eALG? zT$QUIjzy?mM45x)6%;oDBY{DLIi4t2(TBf!*OM69(i?mnY~N8)&I}@aN`!y&(N{$| zQ5>pKoCh3^p(MwOa0;6DQQVE97mCUB=InI!(4j*Uz>0J^91dTc5mvj{3ac&4GDyp; zsKq{c8dIiB8Kaas!js)pQd079mPp4XX~M+g@!uEf_>LVrHd)2IvGGJA@q~{_w)=Ei zSFCYinzJIe%!boRd*(24q!-=p0hdC&YL~$3_F(~I#UCj|hScAiFfj!X&!&@w1-`MR`iYR9$ ziZ@UU%Xs>oenWIhsmpRqJOIx;^URyZm?p1ymK{2C9LGd9 zOZJF7e(uiF($dcgP4cc?yK1fC;4bp!PWGNjH#9U<`w>W`Qg2(u;aQrTk~RX(&CTlz z>3i_O2OIN~S@Am*LxA65I4WbpE&~jP8O&n}3;7sjSz`y6sr00XWhl-7k{xwn`eeOp zlxg&2Gm3LEY989BbR*xOjCDBt3y`@RZl;xu85Zn+jWNb6PDn=_RlWf?OEf=z|&q<+&Hg3R$E*9rySxT zuT9(-b6+P(65C&v4Ie(dr@bfaA~F|stjGTSY@2-9Ir{WM+Gq3#xgrz_U20j@WqJIuKbDr3isxHT1{gee z@HtAUv>*TSNhh6@%H;+FgMgB!o_gvb&(a?V_&TL6EiFCTDsJy0GB{ne~9X|c^ z)7?S>0|ySYPbQnP1!I6dp3y3ZdQAFmk294b9>8OsF` zwgNDn5eP4$xC7Y0d?ukh>%B*2CBglr3{vhq5Oqnv}KyJVe_uQns z2!%r5v5FhIh|C>*96fsU7(W8V#l`mK_Ps1kwt6~yBoc|?h4ht`m7P$S%+r~KVwy#; zXa_;R7=uVyrA4$dv!g~?Z(o?>iEsxF8-a5%ET@h0yxMV=j$;T@i{igA)FDKe&p$Gs zkF0xMV?EnzEG4UjoIQK?+s2rBZ-7Q7lgW`vsZn0_+W!6f7g~asTM>~9fR5dEGMUVm z^7)EItnVGb2*P3G$B#ePN4x8+)n`o?k$F{T`{+2%H0!yt8ItwZc>leRm-foSWOnxE z>0-E@`OMFwbA88B?{EuCkq$}2YgW6(DE}<1x=aG?u@+CrU9G!$a)kbSkL? z%s!D1un!h~Q(%Lz%o?EEeOlJEWnXR)oJyrmFVOx}*kgo@F@LSCt$i*_ z=zp_3M;U_)jCKSQK)O)0m=6C$(x_!SZiV)wU42-)>q~F zSYY+))gjk)CyU6c9GlTEBW8vOI1G3d&4n!7H{{jXm639Og5n1#-bHgQ z3uxRglMvwv%LV!=^SD1>^KIL<_3Yojf0I(`Fl!>~9LM>V&k0+sl)5&x`}-NNPAN6b7_$v{(iroR zQmQv_o>J<3OXf+(ai-=Yv-h_RpI?{maM0i; zjwOoQaeN+O7*WPj&YhGqlXAYB%~FfV@(j!Na3{h^*JQ~0`aUrIB8kXiJ8P^!TH~{Y ztY~v{bD4;&ETE^drlzJS-?N!7foyub)PZ@0`Y0kXt@Ry##?4!$cI43&cU||&s;Vl- zx^|F#91-PbmW!c@vXuz+M0uMiTfOVgW+CLQ_F%ZKd%5qRZ^q|QBJ$*y@Gnmhk^c~p z4I6w}Z$4kK*ETJJ3KeCYb6&AE8j_Y^!MJeGu-3&#Qg2`=VE}$CPrT)v>%-%O)!UWwt zH-P2bjza}5XA)*Cco}qt9i$k^|4<)H`=JRd{=Jhu31~IOm=>i}FW@-##H{;T?H_R* z=hiR!r`-V&hN0Mma2|hVJ;T_D;slS(zh*wS1XF%UTjo}!RGAOb$BZ%eheDx$&sN_Z zHh>Dn5)5}!$?HV8%_4FN!z00TAKGYcZa%VKzkXLLr6vKRfgwOoU^jcxM&C8Ytct~A zi~9EM+mcJqS5M}0ZU&Az8yX>RP00Ef~a0COcKv=|TG!dqw_WULT z@HF!>esUl{0H37#+haN-2(byx*%-DHrUod+(1zgxD);_)cz^)i+cJl|&&nMJ5#~wK zKk%ZsHz0C=gUd2crGzr7QJiF5eS~>_K2{76AeW8&DzFKc;Vh<@KEMbRkK=F)Zy~{y l1PBlyK!5-N0tD!0_<#2 List.contains "priorPermalinks" with + | true -> () + | false -> + log.LogInformation($"Creating index {table}.priorPermalinks...") + let! _ = + rethink { + withTable table + indexCreate "priorPermalinks" + indexOption Multi + write + withRetryOnce conn + } + () | false -> () // Users log on with e-mail match Table.WebLogUser = table with @@ -190,17 +204,27 @@ module Page = withRetryDefault } - /// Retrieve all pages for a web log + /// Retrieve all pages for a web log (excludes text, prior permalinks, and revisions) let findAll (webLogId : WebLogId) = rethink { withTable Table.Page getAll [ webLogId ] (nameof webLogId) - without [ "priorPermalinks", "revisions" ] + without [ "text", "priorPermalinks", "revisions" ] result withRetryDefault } - /// Find a page by its ID + /// Find a page by its ID (including prior permalinks and revisions) + let findByFullId (pageId : PageId) webLogId = + rethink { + withTable Table.Page + get pageId + resultOption + withRetryDefault + } + |> verifyWebLog webLogId (fun it -> it.webLogId) + + /// Find a page by its ID (excludes prior permalinks and revisions) let findById (pageId : PageId) webLogId = rethink { withTable Table.Page @@ -222,16 +246,31 @@ module Page = withRetryDefault } |> tryFirst - - /// Find a page by its ID (including permalinks and revisions) - let findByFullId (pageId : PageId) webLogId = - rethink { + + /// Find the current permalink for a page by a prior permalink + let findCurrentPermalink (permalink : Permalink) (webLogId : WebLogId) = + rethink { withTable Table.Page - get pageId - resultOption + getAll [ permalink ] "priorPermalinks" + filter [ "webLogId", webLogId :> obj ] + pluck [ "permalink" ] + limit 1 + result + withRetryDefault + } + |> tryFirst + + /// Find all pages in the page list for the given web log + let findListed (webLogId : WebLogId) = + rethink { + withTable Table.Page + getAll [ webLogId ] (nameof webLogId) + filter [ "showInPageList", true :> obj ] + without [ "text", "priorPermalinks", "revisions" ] + orderBy "title" + result withRetryDefault } - |> verifyWebLog webLogId (fun it -> it.webLogId) /// Find a list of pages (displayed in admin area) let findPageOfPages (webLogId : WebLogId) pageNbr = @@ -245,6 +284,25 @@ module Page = result withRetryDefault } + + /// Update a page + let update (page : Page) = + rethink { + withTable Table.Page + get page.id + update [ + "title", page.title + "permalink", page.permalink + "updatedOn", page.updatedOn + "showInPageList", page.showInPageList + "text", page.text + "priorPermalinks", page.priorPermalinks + "revisions", page.revisions + ] + write + withRetryDefault + ignoreResult + } /// Functions to manipulate posts module Post = @@ -272,6 +330,19 @@ module Post = } |> tryFirst + /// Find the current permalink for a post by a prior permalink + let findCurrentPermalink (permalink : Permalink) (webLogId : WebLogId) = + rethink { + withTable Table.Post + getAll [ permalink ] "priorPermalinks" + filter [ "webLogId", webLogId :> obj ] + pluck [ "permalink" ] + limit 1 + result + withRetryDefault + } + |> tryFirst + /// Find posts to be displayed on a page let findPageOfPublishedPosts (webLogId : WebLogId) pageNbr postsPerPage = rethink { @@ -300,7 +371,7 @@ module WebLog = ignoreResult } - /// Retrieve web log details by the URL base + /// Retrieve a web log by the URL base let findByHost (url : string) = rethink { withTable Table.WebLog @@ -311,6 +382,15 @@ module WebLog = } |> tryFirst + /// Retrieve a web log by its ID + let findById (webLogId : WebLogId) = + rethink { + withTable Table.WebLog + get webLogId + resultOption + withRetryDefault + } + /// Update web log settings let updateSettings (webLog : WebLog) = rethink { diff --git a/src/MyWebLog.DataCS/Category.cs b/src/MyWebLog.DataCS/Category.cs deleted file mode 100644 index c472dec..0000000 --- a/src/MyWebLog.DataCS/Category.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// A category under which a post may be identfied -/// -public class Category -{ - /// - /// The ID of the category - /// - public string Id { get; set; } = ""; - - /// - /// The displayed name - /// - public string Name { get; set; } = ""; - - /// - /// The slug (used in category URLs) - /// - public string Slug { get; set; } = ""; - - /// - /// A longer description of the category - /// - public string? Description { get; set; } = null; - - /// - /// The parent ID of this category (if a subcategory) - /// - public string? ParentId { get; set; } = null; - - /// - /// The parent of this category (if a subcategory) - /// - public Category? Parent { get; set; } = default; - - /// - /// The posts assigned to this category - /// - public ICollection Posts { get; set; } = default!; -} diff --git a/src/MyWebLog.DataCS/Comment.cs b/src/MyWebLog.DataCS/Comment.cs deleted file mode 100644 index bdcbded..0000000 --- a/src/MyWebLog.DataCS/Comment.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// A comment on a post -/// -public class Comment -{ - /// - /// The ID of the comment - /// - public string Id { get; set; } = ""; - - /// - /// The ID of the post to which this comment applies - /// - public string PostId { get; set; } = ""; - - /// - /// The post to which this comment applies - /// - public Post Post { get; set; } = default!; - - /// - /// The ID of the comment to which this comment is a reply - /// - public string? InReplyToId { get; set; } = null; - - /// - /// The comment to which this comment is a reply - /// - public Comment? InReplyTo { get; set; } = default; - - /// - /// The name of the commentor - /// - public string Name { get; set; } = ""; - - /// - /// The e-mail address of the commentor - /// - public string Email { get; set; } = ""; - - /// - /// The URL of the commentor's personal website - /// - public string? Url { get; set; } = null; - - /// - /// The status of the comment - /// - public CommentStatus Status { get; set; } = CommentStatus.Pending; - - /// - /// When the comment was posted - /// - public DateTime PostedOn { get; set; } = DateTime.UtcNow; - - /// - /// The text of the comment - /// - public string Text { get; set; } = ""; -} diff --git a/src/MyWebLog.DataCS/Enums.cs b/src/MyWebLog.DataCS/Enums.cs deleted file mode 100644 index 3eecc51..0000000 --- a/src/MyWebLog.DataCS/Enums.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// The source format for a revision -/// -public enum RevisionSource -{ - /// Markdown text - Markdown, - /// HTML - Html -} - -/// -/// A level of authorization for a given web log -/// -public enum AuthorizationLevel -{ - /// The user may administer all aspects of a web log - Administrator, - /// The user is a known user of a web log - User -} - -/// -/// Statuses for posts -/// -public enum PostStatus -{ - /// The post should not be publicly available - Draft, - /// The post is publicly viewable - Published -} - -/// -/// Statuses for post comments -/// -public enum CommentStatus -{ - /// The comment is approved - Approved, - /// The comment has yet to be approved - Pending, - /// The comment was unsolicited and unwelcome - Spam -} diff --git a/src/MyWebLog.DataCS/Extensions/CategoryExtensions.cs b/src/MyWebLog.DataCS/Extensions/CategoryExtensions.cs deleted file mode 100644 index 138cc32..0000000 --- a/src/MyWebLog.DataCS/Extensions/CategoryExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace MyWebLog.Data; - -public static class CategoryExtensions -{ - /// - /// Count all categories - /// - /// A count of all categories - public static async Task CountAll(this DbSet db) => - await db.CountAsync().ConfigureAwait(false); - - /// - /// Count top-level categories (those that do not have a parent) - /// - /// A count of all top-level categories - public static async Task CountTopLevel(this DbSet db) => - await db.CountAsync(c => c.ParentId == null).ConfigureAwait(false); -} diff --git a/src/MyWebLog.DataCS/Extensions/PageExtensions.cs b/src/MyWebLog.DataCS/Extensions/PageExtensions.cs deleted file mode 100644 index ea64a82..0000000 --- a/src/MyWebLog.DataCS/Extensions/PageExtensions.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace MyWebLog.Data; - -public static class PageExtensions -{ - /// - /// Count the number of pages - /// - /// The number of pages - public static async Task CountAll(this DbSet db) => - await db.CountAsync().ConfigureAwait(false); - - /// - /// Count the number of pages in the page list - /// - /// The number of pages in the page list - public static async Task CountListed(this DbSet db) => - await db.CountAsync(p => p.ShowInPageList).ConfigureAwait(false); - - /// - /// Retrieve all pages (non-tracked) - /// - /// A list of all pages - public static async Task> FindAll(this DbSet db) => - await db.OrderBy(p => p.Title).ToListAsync().ConfigureAwait(false); - - /// - /// Retrieve a page by its ID (non-tracked) - /// - /// The ID of the page to retrieve - /// The requested page (or null if it is not found) - public static async Task FindById(this DbSet db, string id) => - await db.SingleOrDefaultAsync(p => p.Id == id).ConfigureAwait(false); - - /// - /// Retrieve a page by its ID, including its revisions (non-tracked) - /// - /// The ID of the page to retrieve - /// The requested page (or null if it is not found) - public static async Task FindByIdWithRevisions(this DbSet db, string id) => - await db.Include(p => p.Revisions).SingleOrDefaultAsync(p => p.Id == id).ConfigureAwait(false); - - /// - /// Retrieve a page by its permalink (non-tracked) - /// - /// The permalink - /// The requested page (or null if it is not found) - public static async Task FindByPermalink(this DbSet db, string permalink) => - await db.SingleOrDefaultAsync(p => p.Permalink == permalink).ConfigureAwait(false); - - /// - /// Retrieve a page of pages (non-tracked) - /// - /// The page number to retrieve - /// The pages - public static async Task> FindPageOfPages(this DbSet db, int pageNbr) => - await db.OrderBy(p => p.Title).Skip((pageNbr - 1) * 25).Take(25).ToListAsync().ConfigureAwait(false); - - /// - /// Retrieve a page by its ID (tracked) - /// - /// The ID of the page to retrieve - /// The requested page (or null if it is not found) - public static async Task GetById(this DbSet db, string id) => - await db.AsTracking().Include(p => p.Revisions).SingleOrDefaultAsync(p => p.Id == id).ConfigureAwait(false); -} diff --git a/src/MyWebLog.DataCS/Extensions/PostExtensions.cs b/src/MyWebLog.DataCS/Extensions/PostExtensions.cs deleted file mode 100644 index dd28fe1..0000000 --- a/src/MyWebLog.DataCS/Extensions/PostExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace MyWebLog.Data; - -public static class PostExtensions -{ - /// - /// Count the posts in the given status - /// - /// The status for which posts should be counted - /// A count of the posts in the given status - public static async Task CountByStatus(this DbSet db, PostStatus status) => - await db.CountAsync(p => p.Status == status).ConfigureAwait(false); - - /// - /// Retrieve a post by its permalink (non-tracked) - /// - /// The possible post permalink - /// The post matching the permalink, or null if none is found - public static async Task FindByPermalink(this DbSet db, string permalink) => - await db.SingleOrDefaultAsync(p => p.Id == permalink).ConfigureAwait(false); - - /// - /// Retrieve a page of published posts (non-tracked) - /// - /// The page number to retrieve - /// The number of posts per page - /// A list of posts representing the posts for the given page - public static async Task> FindPageOfPublishedPosts(this DbSet db, int pageNbr, int postsPerPage) => - await db.Where(p => p.Status == PostStatus.Published) - .Skip((pageNbr - 1) * postsPerPage).Take(postsPerPage) - .ToListAsync(); -} diff --git a/src/MyWebLog.DataCS/Extensions/WebLogDetailsExtensions.cs b/src/MyWebLog.DataCS/Extensions/WebLogDetailsExtensions.cs deleted file mode 100644 index 821cbf4..0000000 --- a/src/MyWebLog.DataCS/Extensions/WebLogDetailsExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace MyWebLog.Data; - -public static class WebLogDetailsExtensions -{ - /// - /// Find the details of a web log by its host (non-tracked) - /// - /// The host - /// The web log (or null if not found) - public static async Task FindByHost(this DbSet db, string host) => - await db.FirstOrDefaultAsync(wld => wld.UrlBase == host).ConfigureAwait(false); - - /// - /// Get the details of a web log by its host (tracked) - /// - /// The host - /// The web log (or null if not found) - public static async Task GetByHost(this DbSet db, string host) => - await db.AsTracking().FirstOrDefaultAsync(wld => wld.UrlBase == host).ConfigureAwait(false); -} diff --git a/src/MyWebLog.DataCS/Extensions/WebLogUserExtensions.cs b/src/MyWebLog.DataCS/Extensions/WebLogUserExtensions.cs deleted file mode 100644 index 71d614e..0000000 --- a/src/MyWebLog.DataCS/Extensions/WebLogUserExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace MyWebLog.Data; - -public static class WebLogUserExtensions -{ - /// - /// Find a user by their log on information (non-tracked) - /// - /// The user's e-mail address - /// The hash of the password provided by the user - /// The user, if the credentials match; null if they do not - public static async Task FindByEmail(this DbSet db, string email) => - await db.SingleOrDefaultAsync(wlu => wlu.UserName == email).ConfigureAwait(false); - -} diff --git a/src/MyWebLog.DataCS/Migrations/20220307034307_Initial.Designer.cs b/src/MyWebLog.DataCS/Migrations/20220307034307_Initial.Designer.cs deleted file mode 100644 index 37c93a6..0000000 --- a/src/MyWebLog.DataCS/Migrations/20220307034307_Initial.Designer.cs +++ /dev/null @@ -1,521 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using MyWebLog.Data; - -#nullable disable - -namespace MyWebLog.Data.Migrations -{ - [DbContext(typeof(WebLogDbContext))] - [Migration("20220307034307_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.2"); - - modelBuilder.Entity("CategoryPost", b => - { - b.Property("CategoriesId") - .HasColumnType("TEXT"); - - b.Property("PostsId") - .HasColumnType("TEXT"); - - b.HasKey("CategoriesId", "PostsId"); - - b.HasIndex("PostsId"); - - b.ToTable("CategoryPost"); - }); - - modelBuilder.Entity("MyWebLog.Data.Category", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ParentId") - .HasColumnType("TEXT"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ParentId"); - - b.HasIndex("Slug"); - - b.ToTable("Category"); - }); - - modelBuilder.Entity("MyWebLog.Data.Comment", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Email") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("InReplyToId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PostId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PostedOn") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("InReplyToId"); - - b.HasIndex("PostId"); - - b.ToTable("Comment"); - }); - - modelBuilder.Entity("MyWebLog.Data.Page", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Permalink") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PublishedOn") - .HasColumnType("TEXT"); - - b.Property("ShowInPageList") - .HasColumnType("INTEGER"); - - b.Property("Template") - .HasColumnType("TEXT"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedOn") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("Permalink"); - - b.ToTable("Page"); - }); - - modelBuilder.Entity("MyWebLog.Data.PagePermalink", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("PageId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("PageId"); - - b.ToTable("PagePermalink"); - }); - - modelBuilder.Entity("MyWebLog.Data.PageRevision", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AsOf") - .HasColumnType("TEXT"); - - b.Property("PageId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("PageId"); - - b.ToTable("PageRevision"); - }); - - modelBuilder.Entity("MyWebLog.Data.Post", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Permalink") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PublishedOn") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedOn") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("Permalink"); - - b.ToTable("Post"); - }); - - modelBuilder.Entity("MyWebLog.Data.PostPermalink", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("PostId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("PostId"); - - b.ToTable("PostPermalink"); - }); - - modelBuilder.Entity("MyWebLog.Data.PostRevision", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AsOf") - .HasColumnType("TEXT"); - - b.Property("PostId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("PostId"); - - b.ToTable("PostRevision"); - }); - - modelBuilder.Entity("MyWebLog.Data.Tag", b => - { - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Name"); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("MyWebLog.Data.WebLogDetails", b => - { - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("DefaultPage") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PostsPerPage") - .HasColumnType("INTEGER"); - - b.Property("Subtitle") - .HasColumnType("TEXT"); - - b.Property("ThemePath") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("TimeZone") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UrlBase") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Name"); - - b.ToTable("WebLogDetails"); - }); - - modelBuilder.Entity("MyWebLog.Data.WebLogUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorizationLevel") - .HasColumnType("INTEGER"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PreferredName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Salt") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.Property("UserName") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("WebLogUser"); - }); - - modelBuilder.Entity("PostTag", b => - { - b.Property("PostsId") - .HasColumnType("TEXT"); - - b.Property("TagsName") - .HasColumnType("TEXT"); - - b.HasKey("PostsId", "TagsName"); - - b.HasIndex("TagsName"); - - b.ToTable("PostTag"); - }); - - modelBuilder.Entity("CategoryPost", b => - { - b.HasOne("MyWebLog.Data.Category", null) - .WithMany() - .HasForeignKey("CategoriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("MyWebLog.Data.Post", null) - .WithMany() - .HasForeignKey("PostsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("MyWebLog.Data.Category", b => - { - b.HasOne("MyWebLog.Data.Category", "Parent") - .WithMany() - .HasForeignKey("ParentId"); - - b.Navigation("Parent"); - }); - - modelBuilder.Entity("MyWebLog.Data.Comment", b => - { - b.HasOne("MyWebLog.Data.Comment", "InReplyTo") - .WithMany() - .HasForeignKey("InReplyToId"); - - b.HasOne("MyWebLog.Data.Post", "Post") - .WithMany() - .HasForeignKey("PostId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("InReplyTo"); - - b.Navigation("Post"); - }); - - modelBuilder.Entity("MyWebLog.Data.Page", b => - { - b.HasOne("MyWebLog.Data.WebLogUser", "Author") - .WithMany("Pages") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - }); - - modelBuilder.Entity("MyWebLog.Data.PagePermalink", b => - { - b.HasOne("MyWebLog.Data.Page", "Page") - .WithMany("PriorPermalinks") - .HasForeignKey("PageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Page"); - }); - - modelBuilder.Entity("MyWebLog.Data.PageRevision", b => - { - b.HasOne("MyWebLog.Data.Page", "Page") - .WithMany("Revisions") - .HasForeignKey("PageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Page"); - }); - - modelBuilder.Entity("MyWebLog.Data.Post", b => - { - b.HasOne("MyWebLog.Data.WebLogUser", "Author") - .WithMany("Posts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - }); - - modelBuilder.Entity("MyWebLog.Data.PostPermalink", b => - { - b.HasOne("MyWebLog.Data.Post", "Post") - .WithMany("PriorPermalinks") - .HasForeignKey("PostId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Post"); - }); - - modelBuilder.Entity("MyWebLog.Data.PostRevision", b => - { - b.HasOne("MyWebLog.Data.Post", "Post") - .WithMany("Revisions") - .HasForeignKey("PostId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Post"); - }); - - modelBuilder.Entity("PostTag", b => - { - b.HasOne("MyWebLog.Data.Post", null) - .WithMany() - .HasForeignKey("PostsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("MyWebLog.Data.Tag", null) - .WithMany() - .HasForeignKey("TagsName") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("MyWebLog.Data.Page", b => - { - b.Navigation("PriorPermalinks"); - - b.Navigation("Revisions"); - }); - - modelBuilder.Entity("MyWebLog.Data.Post", b => - { - b.Navigation("PriorPermalinks"); - - b.Navigation("Revisions"); - }); - - modelBuilder.Entity("MyWebLog.Data.WebLogUser", b => - { - b.Navigation("Pages"); - - b.Navigation("Posts"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/MyWebLog.DataCS/Migrations/20220307034307_Initial.cs b/src/MyWebLog.DataCS/Migrations/20220307034307_Initial.cs deleted file mode 100644 index 6381fc9..0000000 --- a/src/MyWebLog.DataCS/Migrations/20220307034307_Initial.cs +++ /dev/null @@ -1,399 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace MyWebLog.Data.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Category", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", nullable: false), - Slug = table.Column(type: "TEXT", nullable: false), - Description = table.Column(type: "TEXT", nullable: true), - ParentId = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Category", x => x.Id); - table.ForeignKey( - name: "FK_Category_Category_ParentId", - column: x => x.ParentId, - principalTable: "Category", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "Tag", - columns: table => new - { - Name = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tag", x => x.Name); - }); - - migrationBuilder.CreateTable( - name: "WebLogDetails", - columns: table => new - { - Name = table.Column(type: "TEXT", nullable: false), - Subtitle = table.Column(type: "TEXT", nullable: true), - DefaultPage = table.Column(type: "TEXT", nullable: false), - PostsPerPage = table.Column(type: "INTEGER", nullable: false), - ThemePath = table.Column(type: "TEXT", nullable: false), - UrlBase = table.Column(type: "TEXT", nullable: false), - TimeZone = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_WebLogDetails", x => x.Name); - }); - - migrationBuilder.CreateTable( - name: "WebLogUser", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - UserName = table.Column(type: "TEXT", nullable: false), - FirstName = table.Column(type: "TEXT", nullable: false), - LastName = table.Column(type: "TEXT", nullable: false), - PreferredName = table.Column(type: "TEXT", nullable: false), - PasswordHash = table.Column(type: "TEXT", nullable: false), - Salt = table.Column(type: "TEXT", nullable: false), - Url = table.Column(type: "TEXT", nullable: true), - AuthorizationLevel = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_WebLogUser", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Page", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - AuthorId = table.Column(type: "TEXT", nullable: false), - Title = table.Column(type: "TEXT", nullable: false), - Permalink = table.Column(type: "TEXT", nullable: false), - PublishedOn = table.Column(type: "TEXT", nullable: false), - UpdatedOn = table.Column(type: "TEXT", nullable: false), - ShowInPageList = table.Column(type: "INTEGER", nullable: false), - Template = table.Column(type: "TEXT", nullable: true), - Text = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Page", x => x.Id); - table.ForeignKey( - name: "FK_Page_WebLogUser_AuthorId", - column: x => x.AuthorId, - principalTable: "WebLogUser", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Post", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - AuthorId = table.Column(type: "TEXT", nullable: false), - Status = table.Column(type: "INTEGER", nullable: false), - Title = table.Column(type: "TEXT", nullable: false), - Permalink = table.Column(type: "TEXT", nullable: false), - PublishedOn = table.Column(type: "TEXT", nullable: true), - UpdatedOn = table.Column(type: "TEXT", nullable: false), - Text = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Post", x => x.Id); - table.ForeignKey( - name: "FK_Post_WebLogUser_AuthorId", - column: x => x.AuthorId, - principalTable: "WebLogUser", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PagePermalink", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - PageId = table.Column(type: "TEXT", nullable: false), - Url = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PagePermalink", x => x.Id); - table.ForeignKey( - name: "FK_PagePermalink_Page_PageId", - column: x => x.PageId, - principalTable: "Page", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PageRevision", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - PageId = table.Column(type: "TEXT", nullable: false), - AsOf = table.Column(type: "TEXT", nullable: false), - SourceType = table.Column(type: "INTEGER", nullable: false), - Text = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PageRevision", x => x.Id); - table.ForeignKey( - name: "FK_PageRevision_Page_PageId", - column: x => x.PageId, - principalTable: "Page", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "CategoryPost", - columns: table => new - { - CategoriesId = table.Column(type: "TEXT", nullable: false), - PostsId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_CategoryPost", x => new { x.CategoriesId, x.PostsId }); - table.ForeignKey( - name: "FK_CategoryPost_Category_CategoriesId", - column: x => x.CategoriesId, - principalTable: "Category", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_CategoryPost_Post_PostsId", - column: x => x.PostsId, - principalTable: "Post", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Comment", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - PostId = table.Column(type: "TEXT", nullable: false), - InReplyToId = table.Column(type: "TEXT", nullable: true), - Name = table.Column(type: "TEXT", nullable: false), - Email = table.Column(type: "TEXT", nullable: false), - Url = table.Column(type: "TEXT", nullable: true), - Status = table.Column(type: "INTEGER", nullable: false), - PostedOn = table.Column(type: "TEXT", nullable: false), - Text = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Comment", x => x.Id); - table.ForeignKey( - name: "FK_Comment_Comment_InReplyToId", - column: x => x.InReplyToId, - principalTable: "Comment", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_Comment_Post_PostId", - column: x => x.PostId, - principalTable: "Post", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PostPermalink", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - PostId = table.Column(type: "TEXT", nullable: false), - Url = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PostPermalink", x => x.Id); - table.ForeignKey( - name: "FK_PostPermalink_Post_PostId", - column: x => x.PostId, - principalTable: "Post", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PostRevision", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - PostId = table.Column(type: "TEXT", nullable: false), - AsOf = table.Column(type: "TEXT", nullable: false), - SourceType = table.Column(type: "INTEGER", nullable: false), - Text = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PostRevision", x => x.Id); - table.ForeignKey( - name: "FK_PostRevision_Post_PostId", - column: x => x.PostId, - principalTable: "Post", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PostTag", - columns: table => new - { - PostsId = table.Column(type: "TEXT", nullable: false), - TagsName = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PostTag", x => new { x.PostsId, x.TagsName }); - table.ForeignKey( - name: "FK_PostTag_Post_PostsId", - column: x => x.PostsId, - principalTable: "Post", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_PostTag_Tag_TagsName", - column: x => x.TagsName, - principalTable: "Tag", - principalColumn: "Name", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Category_ParentId", - table: "Category", - column: "ParentId"); - - migrationBuilder.CreateIndex( - name: "IX_Category_Slug", - table: "Category", - column: "Slug"); - - migrationBuilder.CreateIndex( - name: "IX_CategoryPost_PostsId", - table: "CategoryPost", - column: "PostsId"); - - migrationBuilder.CreateIndex( - name: "IX_Comment_InReplyToId", - table: "Comment", - column: "InReplyToId"); - - migrationBuilder.CreateIndex( - name: "IX_Comment_PostId", - table: "Comment", - column: "PostId"); - - migrationBuilder.CreateIndex( - name: "IX_Page_AuthorId", - table: "Page", - column: "AuthorId"); - - migrationBuilder.CreateIndex( - name: "IX_Page_Permalink", - table: "Page", - column: "Permalink"); - - migrationBuilder.CreateIndex( - name: "IX_PagePermalink_PageId", - table: "PagePermalink", - column: "PageId"); - - migrationBuilder.CreateIndex( - name: "IX_PageRevision_PageId", - table: "PageRevision", - column: "PageId"); - - migrationBuilder.CreateIndex( - name: "IX_Post_AuthorId", - table: "Post", - column: "AuthorId"); - - migrationBuilder.CreateIndex( - name: "IX_Post_Permalink", - table: "Post", - column: "Permalink"); - - migrationBuilder.CreateIndex( - name: "IX_PostPermalink_PostId", - table: "PostPermalink", - column: "PostId"); - - migrationBuilder.CreateIndex( - name: "IX_PostRevision_PostId", - table: "PostRevision", - column: "PostId"); - - migrationBuilder.CreateIndex( - name: "IX_PostTag_TagsName", - table: "PostTag", - column: "TagsName"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "CategoryPost"); - - migrationBuilder.DropTable( - name: "Comment"); - - migrationBuilder.DropTable( - name: "PagePermalink"); - - migrationBuilder.DropTable( - name: "PageRevision"); - - migrationBuilder.DropTable( - name: "PostPermalink"); - - migrationBuilder.DropTable( - name: "PostRevision"); - - migrationBuilder.DropTable( - name: "PostTag"); - - migrationBuilder.DropTable( - name: "WebLogDetails"); - - migrationBuilder.DropTable( - name: "Category"); - - migrationBuilder.DropTable( - name: "Page"); - - migrationBuilder.DropTable( - name: "Post"); - - migrationBuilder.DropTable( - name: "Tag"); - - migrationBuilder.DropTable( - name: "WebLogUser"); - } - } -} diff --git a/src/MyWebLog.DataCS/Migrations/WebLogDbContextModelSnapshot.cs b/src/MyWebLog.DataCS/Migrations/WebLogDbContextModelSnapshot.cs deleted file mode 100644 index bd813ee..0000000 --- a/src/MyWebLog.DataCS/Migrations/WebLogDbContextModelSnapshot.cs +++ /dev/null @@ -1,519 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using MyWebLog.Data; - -#nullable disable - -namespace MyWebLog.Data.Migrations -{ - [DbContext(typeof(WebLogDbContext))] - partial class WebLogDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.2"); - - modelBuilder.Entity("CategoryPost", b => - { - b.Property("CategoriesId") - .HasColumnType("TEXT"); - - b.Property("PostsId") - .HasColumnType("TEXT"); - - b.HasKey("CategoriesId", "PostsId"); - - b.HasIndex("PostsId"); - - b.ToTable("CategoryPost"); - }); - - modelBuilder.Entity("MyWebLog.Data.Category", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ParentId") - .HasColumnType("TEXT"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ParentId"); - - b.HasIndex("Slug"); - - b.ToTable("Category"); - }); - - modelBuilder.Entity("MyWebLog.Data.Comment", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Email") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("InReplyToId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PostId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PostedOn") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("InReplyToId"); - - b.HasIndex("PostId"); - - b.ToTable("Comment"); - }); - - modelBuilder.Entity("MyWebLog.Data.Page", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Permalink") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PublishedOn") - .HasColumnType("TEXT"); - - b.Property("ShowInPageList") - .HasColumnType("INTEGER"); - - b.Property("Template") - .HasColumnType("TEXT"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedOn") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("Permalink"); - - b.ToTable("Page"); - }); - - modelBuilder.Entity("MyWebLog.Data.PagePermalink", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("PageId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("PageId"); - - b.ToTable("PagePermalink"); - }); - - modelBuilder.Entity("MyWebLog.Data.PageRevision", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AsOf") - .HasColumnType("TEXT"); - - b.Property("PageId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("PageId"); - - b.ToTable("PageRevision"); - }); - - modelBuilder.Entity("MyWebLog.Data.Post", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Permalink") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PublishedOn") - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedOn") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("Permalink"); - - b.ToTable("Post"); - }); - - modelBuilder.Entity("MyWebLog.Data.PostPermalink", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("PostId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("PostId"); - - b.ToTable("PostPermalink"); - }); - - modelBuilder.Entity("MyWebLog.Data.PostRevision", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AsOf") - .HasColumnType("TEXT"); - - b.Property("PostId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("Text") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("PostId"); - - b.ToTable("PostRevision"); - }); - - modelBuilder.Entity("MyWebLog.Data.Tag", b => - { - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Name"); - - b.ToTable("Tag"); - }); - - modelBuilder.Entity("MyWebLog.Data.WebLogDetails", b => - { - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("DefaultPage") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PostsPerPage") - .HasColumnType("INTEGER"); - - b.Property("Subtitle") - .HasColumnType("TEXT"); - - b.Property("ThemePath") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("TimeZone") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UrlBase") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Name"); - - b.ToTable("WebLogDetails"); - }); - - modelBuilder.Entity("MyWebLog.Data.WebLogUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorizationLevel") - .HasColumnType("INTEGER"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PreferredName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Salt") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.Property("UserName") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("WebLogUser"); - }); - - modelBuilder.Entity("PostTag", b => - { - b.Property("PostsId") - .HasColumnType("TEXT"); - - b.Property("TagsName") - .HasColumnType("TEXT"); - - b.HasKey("PostsId", "TagsName"); - - b.HasIndex("TagsName"); - - b.ToTable("PostTag"); - }); - - modelBuilder.Entity("CategoryPost", b => - { - b.HasOne("MyWebLog.Data.Category", null) - .WithMany() - .HasForeignKey("CategoriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("MyWebLog.Data.Post", null) - .WithMany() - .HasForeignKey("PostsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("MyWebLog.Data.Category", b => - { - b.HasOne("MyWebLog.Data.Category", "Parent") - .WithMany() - .HasForeignKey("ParentId"); - - b.Navigation("Parent"); - }); - - modelBuilder.Entity("MyWebLog.Data.Comment", b => - { - b.HasOne("MyWebLog.Data.Comment", "InReplyTo") - .WithMany() - .HasForeignKey("InReplyToId"); - - b.HasOne("MyWebLog.Data.Post", "Post") - .WithMany() - .HasForeignKey("PostId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("InReplyTo"); - - b.Navigation("Post"); - }); - - modelBuilder.Entity("MyWebLog.Data.Page", b => - { - b.HasOne("MyWebLog.Data.WebLogUser", "Author") - .WithMany("Pages") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - }); - - modelBuilder.Entity("MyWebLog.Data.PagePermalink", b => - { - b.HasOne("MyWebLog.Data.Page", "Page") - .WithMany("PriorPermalinks") - .HasForeignKey("PageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Page"); - }); - - modelBuilder.Entity("MyWebLog.Data.PageRevision", b => - { - b.HasOne("MyWebLog.Data.Page", "Page") - .WithMany("Revisions") - .HasForeignKey("PageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Page"); - }); - - modelBuilder.Entity("MyWebLog.Data.Post", b => - { - b.HasOne("MyWebLog.Data.WebLogUser", "Author") - .WithMany("Posts") - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - }); - - modelBuilder.Entity("MyWebLog.Data.PostPermalink", b => - { - b.HasOne("MyWebLog.Data.Post", "Post") - .WithMany("PriorPermalinks") - .HasForeignKey("PostId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Post"); - }); - - modelBuilder.Entity("MyWebLog.Data.PostRevision", b => - { - b.HasOne("MyWebLog.Data.Post", "Post") - .WithMany("Revisions") - .HasForeignKey("PostId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Post"); - }); - - modelBuilder.Entity("PostTag", b => - { - b.HasOne("MyWebLog.Data.Post", null) - .WithMany() - .HasForeignKey("PostsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("MyWebLog.Data.Tag", null) - .WithMany() - .HasForeignKey("TagsName") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("MyWebLog.Data.Page", b => - { - b.Navigation("PriorPermalinks"); - - b.Navigation("Revisions"); - }); - - modelBuilder.Entity("MyWebLog.Data.Post", b => - { - b.Navigation("PriorPermalinks"); - - b.Navigation("Revisions"); - }); - - modelBuilder.Entity("MyWebLog.Data.WebLogUser", b => - { - b.Navigation("Pages"); - - b.Navigation("Posts"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/MyWebLog.DataCS/MyWebLog.DataCS.csproj b/src/MyWebLog.DataCS/MyWebLog.DataCS.csproj deleted file mode 100644 index 3f95143..0000000 --- a/src/MyWebLog.DataCS/MyWebLog.DataCS.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net6.0 - enable - enable - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - True - - diff --git a/src/MyWebLog.DataCS/Page.cs b/src/MyWebLog.DataCS/Page.cs deleted file mode 100644 index 7233fdb..0000000 --- a/src/MyWebLog.DataCS/Page.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// A page (text not associated with a date/time) -/// -public class Page -{ - /// - /// The ID of this page - /// - public string Id { get; set; } = ""; - - /// - /// The ID of the author of this page - /// - public string AuthorId { get; set; } = ""; - - /// - /// The author of this page - /// - public WebLogUser Author { get; set; } = default!; - - /// - /// The title of the page - /// - public string Title { get; set; } = ""; - - /// - /// The link at which this page is displayed - /// - public string Permalink { get; set; } = ""; - - /// - /// The instant this page was published - /// - public DateTime PublishedOn { get; set; } = DateTime.MinValue; - - /// - /// The instant this page was last updated - /// - public DateTime UpdatedOn { get; set; } = DateTime.MinValue; - - /// - /// Whether this page shows as part of the web log's navigation - /// - public bool ShowInPageList { get; set; } = false; - - /// - /// The template to use when rendering this page - /// - public string? Template { get; set; } = null; - - /// - /// The current text of the page - /// - public string Text { get; set; } = ""; - - /// - /// Permalinks at which this page may have been previously served (useful for migrated content) - /// - public ICollection PriorPermalinks { get; set; } = default!; - - /// - /// Revisions of this page - /// - public ICollection Revisions { get; set; } = default!; -} diff --git a/src/MyWebLog.DataCS/Permalink.cs b/src/MyWebLog.DataCS/Permalink.cs deleted file mode 100644 index b36ceca..0000000 --- a/src/MyWebLog.DataCS/Permalink.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// A permalink which a post or page used to have -/// -public abstract class Permalink -{ - /// - /// The ID of this permalink - /// - public string Id { get; set; } = ""; - - /// - /// The link - /// - public string Url { get; set; } = ""; -} - -/// -/// A prior permalink for a page -/// -public class PagePermalink : Permalink -{ - /// - /// The ID of the page to which this permalink belongs - /// - public string PageId { get; set; } = ""; - - /// - /// The page to which this permalink belongs - /// - public Page Page { get; set; } = default!; -} - -/// -/// A prior permalink for a post -/// -public class PostPermalink : Permalink -{ - /// - /// The ID of the post to which this permalink belongs - /// - public string PostId { get; set; } = ""; - - /// - /// The post to which this permalink belongs - /// - public Post Post { get; set; } = default!; -} diff --git a/src/MyWebLog.DataCS/Post.cs b/src/MyWebLog.DataCS/Post.cs deleted file mode 100644 index f2afe39..0000000 --- a/src/MyWebLog.DataCS/Post.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// A web log post -/// -public class Post -{ - /// - /// The ID of this post - /// - public string Id { get; set; } = ""; - - /// - /// The ID of the author of this post - /// - public string AuthorId { get; set; } = ""; - - /// - /// The author of the post - /// - public WebLogUser Author { get; set; } = default!; - - /// - /// The status - /// - public PostStatus Status { get; set; } = PostStatus.Draft; - - /// - /// The title - /// - public string Title { get; set; } = ""; - - /// - /// The link at which the post resides - /// - public string Permalink { get; set; } = ""; - - /// - /// The instant on which the post was originally published - /// - public DateTime? PublishedOn { get; set; } = null; - - /// - /// The instant on which the post was last updated - /// - public DateTime UpdatedOn { get; set; } = DateTime.MinValue; - - /// - /// The text of the post in HTML (ready to display) format - /// - public string Text { get; set; } = ""; - - /// - /// The Ids of the categories to which this is assigned - /// - public ICollection Categories { get; set; } = default!; - - /// - /// The tags for the post - /// - public ICollection Tags { get; set; } = default!; - - /// - /// Permalinks at which this post may have been previously served (useful for migrated content) - /// - public ICollection PriorPermalinks { get; set; } = default!; - - /// - /// The revisions for this post - /// - public ICollection Revisions { get; set; } = default!; -} diff --git a/src/MyWebLog.DataCS/Revision.cs b/src/MyWebLog.DataCS/Revision.cs deleted file mode 100644 index cd50875..0000000 --- a/src/MyWebLog.DataCS/Revision.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// A revision of a page or post -/// -public abstract class Revision -{ - /// - /// The ID of this revision - /// - public string Id { get; set; } = ""; - - /// - /// When this revision was saved - /// - public DateTime AsOf { get; set; } = DateTime.UtcNow; - - /// - /// The source language (Markdown or HTML) - /// - public RevisionSource SourceType { get; set; } = RevisionSource.Html; - - /// - /// The text of the revision - /// - public string Text { get; set; } = ""; -} - -/// -/// A revision of a page -/// -public class PageRevision : Revision -{ - /// - /// The ID of the page to which this revision belongs - /// - public string PageId { get; set; } = ""; - - /// - /// The page to which this revision belongs - /// - public Page Page { get; set; } = default!; -} - -/// -/// A revision of a post -/// -public class PostRevision : Revision -{ - /// - /// The ID of the post to which this revision applies - /// - public string PostId { get; set; } = ""; - - /// - /// The post to which this revision applies - /// - public Post Post { get; set; } = default!; -} diff --git a/src/MyWebLog.DataCS/Tag.cs b/src/MyWebLog.DataCS/Tag.cs deleted file mode 100644 index 28f07ef..0000000 --- a/src/MyWebLog.DataCS/Tag.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// A tag -/// -public class Tag -{ - /// - /// The name of the tag - /// - public string Name { get; set; } = ""; - - /// - /// The posts with this tag assigned - /// - public ICollection Posts { get; set; } = default!; -} diff --git a/src/MyWebLog.DataCS/WebLogDbContext.cs b/src/MyWebLog.DataCS/WebLogDbContext.cs deleted file mode 100644 index 3d63b97..0000000 --- a/src/MyWebLog.DataCS/WebLogDbContext.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace MyWebLog.Data; - -/// -/// Data context for web log data -/// -public sealed class WebLogDbContext : DbContext -{ - /// - /// Create a new ID (short GUID) - /// - /// A new short GUID - /// https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID - public static string NewId() => - Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('/', '_').Replace('+', '-')[..22]; - - /// - /// The categories for the web log - /// - public DbSet Categories { get; set; } = default!; - - /// - /// Comments on posts - /// - public DbSet Comments { get; set; } = default!; - - /// - /// Pages - /// - public DbSet Pages { get; set; } = default!; - - /// - /// Web log posts - /// - public DbSet Posts { get; set; } = default!; - - /// - /// Post tags - /// - public DbSet Tags { get; set; } = default!; - - /// - /// The users of the web log - /// - public DbSet Users { get; set; } = default!; - - /// - /// The details for the web log - /// - public DbSet WebLogDetails { get; set; } = default!; - - /// - /// Constructor - /// - /// Configuration options - public WebLogDbContext(DbContextOptions options) : base(options) { } - - /// - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - } - - /// - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - // Make tables use singular names - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) - entityType.SetTableName(entityType.DisplayName().Split(' ')[0]); - - // Tag and WebLogDetails use Name as its ID - modelBuilder.Entity().HasKey(t => t.Name); - modelBuilder.Entity().HasKey(wld => wld.Name); - - // Index slugs and links - modelBuilder.Entity().HasIndex(c => c.Slug); - modelBuilder.Entity().HasIndex(p => p.Permalink); - modelBuilder.Entity().HasIndex(p => p.Permalink); - - // Link "author" to "user" - modelBuilder.Entity().HasOne(p => p.Author).WithMany(wbu => wbu.Pages).HasForeignKey(p => p.AuthorId); - modelBuilder.Entity().HasOne(p => p.Author).WithMany(wbu => wbu.Posts).HasForeignKey(p => p.AuthorId); - } -} diff --git a/src/MyWebLog.DataCS/WebLogDetails.cs b/src/MyWebLog.DataCS/WebLogDetails.cs deleted file mode 100644 index e6e1d9a..0000000 --- a/src/MyWebLog.DataCS/WebLogDetails.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// The details about a web log -/// -public class WebLogDetails -{ - /// - /// The name of the web log - /// - public string Name { get; set; } = ""; - - /// - /// A subtitle for the web log - /// - public string? Subtitle { get; set; } = null; - - /// - /// The default page ("posts" or a page Id) - /// - public string DefaultPage { get; set; } = ""; - - /// - /// The number of posts to display on pages of posts - /// - public byte PostsPerPage { get; set; } = 10; - - /// - /// The path of the theme (within /views/themes) - /// - public string ThemePath { get; set; } = "Default"; - - /// - /// The URL base - /// - public string UrlBase { get; set; } = ""; - - /// - /// The time zone in which dates/times should be displayed - /// - public string TimeZone { get; set; } = ""; -} diff --git a/src/MyWebLog.DataCS/WebLogUser.cs b/src/MyWebLog.DataCS/WebLogUser.cs deleted file mode 100644 index 3cec46c..0000000 --- a/src/MyWebLog.DataCS/WebLogUser.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace MyWebLog.Data; - -/// -/// A user of the web log -/// -public class WebLogUser -{ - /// - /// The ID of the user - /// - public string Id { get; set; } = ""; - - /// - /// The user name (e-mail address) - /// - public string UserName { get; set; } = ""; - - /// - /// The user's first name - /// - public string FirstName { get; set; } = ""; - - /// - /// The user's last name - /// - public string LastName { get; set; } = ""; - - /// - /// The user's preferred name - /// - public string PreferredName { get; set; } = ""; - - /// - /// The hash of the user's password - /// - public string PasswordHash { get; set; } = ""; - - /// - /// Salt used to calculate the user's password hash - /// - public Guid Salt { get; set; } = Guid.Empty; - - /// - /// The URL of the user's personal site - /// - public string? Url { get; set; } = null; - - /// - /// The user's authorization level - /// - public AuthorizationLevel AuthorizationLevel { get; set; } = AuthorizationLevel.User; - - /// - /// Pages written by this author - /// - public ICollection Pages { get; set; } = default!; - - /// - /// Posts written by this author - /// - public ICollection Posts { get; set; } = default!; -} diff --git a/src/MyWebLog.Domain/DataTypes.fs b/src/MyWebLog.Domain/DataTypes.fs index d430749..534cc64 100644 --- a/src/MyWebLog.Domain/DataTypes.fs +++ b/src/MyWebLog.Domain/DataTypes.fs @@ -2,7 +2,7 @@ open System -/// A category under which a post may be identfied +/// A category under which a post may be identified [] type Category = { /// The ID of the category @@ -120,7 +120,7 @@ type Page = text : string /// Permalinks at which this page may have been previously served (useful for migrated content) - priorPermalinks : string list + priorPermalinks : Permalink list /// Revisions of this page revisions : Revision list diff --git a/src/MyWebLog.Domain/SupportTypes.fs b/src/MyWebLog.Domain/SupportTypes.fs index f955bf1..1c5f3ad 100644 --- a/src/MyWebLog.Domain/SupportTypes.fs +++ b/src/MyWebLog.Domain/SupportTypes.fs @@ -61,6 +61,14 @@ type RevisionSource = /// HTML | Html +/// Functions to support revision sources +module RevisionSource = + + /// Convert a revision source to a string representation + let toString = function Markdown -> "Markdown" | Html -> "HTML" + + /// Convert a string to a revision source + let ofString = function "Markdown" -> Markdown | "HTML" -> Html | x -> invalidArg "string" x /// A revision of a page or post [] diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index a13c5c3..a53376e 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -1,5 +1,43 @@ namespace MyWebLog.ViewModels +open MyWebLog +open System + +/// Details about a page used to display page lists +type DisplayPage = + { /// The ID of this page + id : string + + /// The title of the page + title : string + + /// The link at which this page is displayed + permalink : string + + /// When this page was published + publishedOn : DateTime + + /// When this page was last updated + updatedOn : DateTime + + /// Whether this page shows as part of the web log's navigation + showInPageList : bool + + /// Is this the default page? + isDefault : bool + } + /// Create a display page from a database page + static member fromPage webLog (page : Page) = + let pageId = PageId.toString page.id + { id = pageId + title = page.title + permalink = Permalink.toString page.permalink + publishedOn = page.publishedOn + updatedOn = page.updatedOn + showInPageList = page.showInPageList + isDefault = pageId = webLog.defaultPage + } + /// The model to use to allow a user to log on [] @@ -34,6 +72,42 @@ type DashboardModel = } +/// View model to edit a page +[] +type EditPageModel = + { /// The ID of the page being edited + pageId : string + + /// The title of the page + title : string + + /// The permalink for the page + permalink : string + + /// Whether this page is shown in the page list + isShownInPageList : bool + + /// The source format for the text + source : string + + /// The text of the page + text : string + } + /// Create an edit model from an existing page + static member fromPage (page : Page) = + let latest = + match page.revisions |> List.sortByDescending (fun r -> r.asOf) |> List.tryHead with + | Some rev -> rev + | None -> Revision.empty + { pageId = PageId.toString page.id + title = page.title + permalink = Permalink.toString page.permalink + isShownInPageList = page.showInPageList + source = RevisionSource.toString latest.sourceType + text = latest.text + } + + /// View model for editing web log settings [] type SettingsModel = diff --git a/src/MyWebLog.Migrate/Program.fs b/src/MyWebLog.Migrate/Program.fs deleted file mode 100644 index 7d6dafd..0000000 --- a/src/MyWebLog.Migrate/Program.fs +++ /dev/null @@ -1,178 +0,0 @@ -module Program - -open MyWebLog -open MyWebLog.Data.RethinkDB -open MyWebLog.Entities -open Nancy.Cryptography -open Newtonsoft.Json -open Newtonsoft.Json.Linq -open NodaTime -open RethinkDb.Driver -open System -open System.Linq - -let r = RethinkDB.R - -let appCfg = try AppConfig.FromJson (System.IO.File.ReadAllText "config.json") - with ex -> raise <| Exception ("Bad config.json file", ex) -let cfg = appCfg.DataConfig -// DataConfig.Connect -// (JsonConvert.DeserializeObject("""{ "hostname" : "data01", "authKey" : "1d9a76f8-2d85-4033-be15-1f4313a96bb2", "database" : "myWebLog" }""")) -let conn = cfg.Conn -let toTicks (dt : DateTime) = Instant.FromDateTimeUtc(dt.ToUniversalTime()).ToUnixTimeTicks () -/// Hash the user's password -let pbkdf2 (pw : string) = - PassphraseKeyGenerator(pw, appCfg.PasswordSalt, 4096).GetBytes 512 - |> Seq.fold (fun acc byt -> sprintf "%s%s" acc (byt.ToString "x2")) "" - -let migr8 () = - SetUp.startUpCheck cfg - - Console.WriteLine "Migrating web logs..." - - r.Db("MyWebLog").Table(Table.WebLog) - .RunCursor(conn) - |> Seq.iter (fun x -> - r.Db("myWebLog").Table(Table.WebLog) - .Insert({ Id = string x.["id"] - Name = string x.["name"] - Subtitle = Some <| string x.["subtitle"] - DefaultPage = string x.["defaultPage"] - ThemePath = string x.["themePath"] - TimeZone = string x.["timeZone"] - UrlBase = string x.["urlBase"] - PageList = [] - }) - .RunResult(conn) - |> ignore) - - Console.WriteLine "Migrating users..." - - r.Db("MyWebLog").Table(Table.User) - .RunCursor(conn) - |> Seq.iter (fun x -> - r.Db("myWebLog").Table(Table.User) - .Insert({ Id = string x.["id"] - UserName = string x.["userName"] - FirstName = string x.["firstName"] - LastName = string x.["lastName"] - PreferredName = string x.["preferredName"] - PasswordHash = string x.["passwordHash"] - Url = Some <| string x.["url"] - Authorizations = x.["authorizations"] :?> JArray - |> Seq.map (fun y -> { WebLogId = string y.["webLogId"] - Level = string y.["level"] }) - |> Seq.toList - }) - .RunResult(conn) - |> ignore) - - Console.WriteLine "Migrating categories..." - - r.Db("MyWebLog").Table(Table.Category) - .RunCursor(conn) - |> Seq.iter (fun x -> - r.Db("myWebLog").Table(Table.Category) - .Insert({ Id = string x.["id"] - WebLogId = string x.["webLogId"] - Name = string x.["name"] - Slug = string x.["slug"] - Description = match String.IsNullOrEmpty(string x.["description"]) with - | true -> None - | _ -> Some <| string x.["description"] - ParentId = match String.IsNullOrEmpty(string x.["parentId"]) with - | true -> None - | _ -> Some <| string x.["parentId"] - Children = x.["children"] :?> JArray - |> Seq.map (fun y -> string y) - |> Seq.toList - }) - .RunResult(conn) - |> ignore) - - Console.WriteLine "Migrating comments..." - - r.Db("MyWebLog").Table(Table.Comment) - .RunCursor(conn) - |> Seq.iter (fun x -> - r.Db("myWebLog").Table(Table.Comment) - .Insert({ Id = string x.["id"] - PostId = string x.["postId"] - InReplyToId = match String.IsNullOrEmpty(string x.["inReplyToId"]) with - | true -> None - | _ -> Some <| string x.["inReplyToId"] - Name = string x.["name"] - Email = string x.["email"] - Url = match String.IsNullOrEmpty(string x.["url"]) with - | true -> None - | _ -> Some <| string x.["url"] - Status = string x.["status"] - PostedOn = x.["postedDate"].ToObject() |> toTicks - Text = string x.["text"] - }) - .RunResult(conn) - |> ignore) - - Console.WriteLine "Migrating pages..." - - r.Db("MyWebLog").Table(Table.Page) - .RunCursor(conn) - |> Seq.iter (fun x -> - r.Db("myWebLog").Table(Table.Page) - .Insert({ Id = string x.["id"] - WebLogId = string x.["webLogId"] - AuthorId = string x.["authorId"] - Title = string x.["title"] - Permalink = string x.["permalink"] - PublishedOn = x.["publishedDate"].ToObject () |> toTicks - UpdatedOn = x.["lastUpdatedDate"].ToObject () |> toTicks - ShowInPageList = x.["showInPageList"].ToObject() - Text = string x.["text"] - Revisions = [{ AsOf = x.["lastUpdatedDate"].ToObject () |> toTicks - SourceType = RevisionSource.HTML - Text = string x.["text"] - }] - }) - .RunResult(conn) - |> ignore) - - Console.WriteLine "Migrating posts..." - - r.Db("MyWebLog").Table(Table.Post) - .RunCursor(conn) - |> Seq.iter (fun x -> - r.Db("myWebLog").Table(Table.Post) - .Insert({ Id = string x.["id"] - WebLogId = string x.["webLogId"] - AuthorId = "9b491a0f-48df-4b7b-8c10-120b5cd02895" - Status = string x.["status"] - Title = string x.["title"] - Permalink = string x.["permalink"] - PublishedOn = match x.["publishedDate"] with - | null -> int64 0 - | dt -> dt.ToObject () |> toTicks - UpdatedOn = x.["lastUpdatedDate"].ToObject () |> toTicks - Revisions = [{ AsOf = x.["lastUpdatedDate"].ToObject () - |> toTicks - SourceType = RevisionSource.HTML - Text = string x.["text"] - }] - Text = string x.["text"] - Tags = x.["tag"] :?> JArray - |> Seq.map (fun y -> string y) - |> Seq.toList - CategoryIds = x.["category"] :?> JArray - |> Seq.map (fun y -> string y) - |> Seq.toList - PriorPermalinks = [] - Categories = [] - Comments = [] - }) - .RunResult(conn) - |> ignore) - - -[] -let main argv = - migr8 () - 0 // return an integer exit code diff --git a/src/MyWebLog.Migrate/project.json b/src/MyWebLog.Migrate/project.json deleted file mode 100644 index 9131f89..0000000 --- a/src/MyWebLog.Migrate/project.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "version": "1.0.0-*", - "buildOptions": { - "debugType": "portable", - "emitEntryPoint": true, - "compilerName": "fsc", - "compile": { - "includeFiles": [ - "Program.fs" - ] - } - }, - "dependencies": { - "MyWebLog.App": "0.9.2", - "MyWebLog.Data.RethinkDB": "0.9.2", - "MyWebLog.Entities": "0.9.2", - "NodaTime": "2.0.0-alpha20160729" - }, - "tools": { - "dotnet-compile-fsc":"1.0.0-preview2-*" - }, - "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.1" - }, - "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160629" - } - } - } -} diff --git a/src/MyWebLog.sln b/src/MyWebLog.sln index c7e1bce..840f5d6 100644 --- a/src/MyWebLog.sln +++ b/src/MyWebLog.sln @@ -7,12 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.Themes.BitBadger", EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Domain", "MyWebLog.Domain\MyWebLog.Domain.fsproj", "{8CA99122-888A-4524-8C1B-685F0A4B7B4B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.DataCS", "MyWebLog.DataCS\MyWebLog.DataCS.csproj", "{C9129BED-E4AE-41BB-BDB2-5418B7F924CC}" -EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Data", "MyWebLog.Data\MyWebLog.Data.fsproj", "{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.CS", "MyWebLog.CS\MyWebLog.CS.csproj", "{B23A8093-28B1-4CB5-93F1-B4659516B74F}" -EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyWebLog", "MyWebLog\MyWebLog.fsproj", "{5655B63D-429F-4CCD-A14C-FBD74D987ECB}" EndProject Global @@ -29,18 +25,10 @@ Global {8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU {8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Release|Any CPU.Build.0 = Release|Any CPU - {C9129BED-E4AE-41BB-BDB2-5418B7F924CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9129BED-E4AE-41BB-BDB2-5418B7F924CC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9129BED-E4AE-41BB-BDB2-5418B7F924CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9129BED-E4AE-41BB-BDB2-5418B7F924CC}.Release|Any CPU.Build.0 = Release|Any CPU {D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Release|Any CPU.Build.0 = Release|Any CPU - {B23A8093-28B1-4CB5-93F1-B4659516B74F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B23A8093-28B1-4CB5-93F1-B4659516B74F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B23A8093-28B1-4CB5-93F1-B4659516B74F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B23A8093-28B1-4CB5-93F1-B4659516B74F}.Release|Any CPU.Build.0 = Release|Any CPU {5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU {5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/MyWebLog/Caches.fs b/src/MyWebLog/Caches.fs new file mode 100644 index 0000000..9380240 --- /dev/null +++ b/src/MyWebLog/Caches.fs @@ -0,0 +1,74 @@ +namespace MyWebLog + +open Microsoft.AspNetCore.Http + +/// Helper functions for caches +module Cache = + + /// Create the cache key for the web log for the current request + let makeKey (ctx : HttpContext) = ctx.Request.Host.ToUriComponent () + + +open System.Collections.Concurrent + +/// +/// In-memory cache of web log details +/// +/// This is filled by the middleware via the first request for each host, and can be updated via the web log +/// settings update page +module WebLogCache = + + /// The cache of web log details + let private _cache = ConcurrentDictionary () + + /// Does a host exist in the cache? + let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) + + /// Get the web log for the current request + let get ctx = _cache[Cache.makeKey ctx] + + /// Cache the web log for a particular host + let set ctx webLog = _cache[Cache.makeKey ctx] <- webLog + + +/// A cache of page information needed to display the page list in templates +module PageListCache = + + open Microsoft.Extensions.DependencyInjection + open MyWebLog.ViewModels + open RethinkDb.Driver.Net + + /// Cache of displayed pages + let private _cache = ConcurrentDictionary () + + /// Get the pages for the web log for this request + let get ctx = _cache[Cache.makeKey ctx] + + /// Update the pages for the current web log + let update ctx = task { + let webLog = WebLogCache.get ctx + let conn = ctx.RequestServices.GetRequiredService () + let! pages = Data.Page.findListed webLog.id conn + _cache[Cache.makeKey ctx] <- pages |> List.map (DisplayPage.fromPage webLog) |> Array.ofList + } + +/// Cache for parsed templates +module TemplateCache = + + open DotLiquid + open System.IO + + /// Cache of parsed templates + let private _cache = ConcurrentDictionary () + + /// Get a template for the given theme and template nate + let get (theme : string) (templateName : string) = task { + let templatePath = $"themes/{theme}/{templateName}" + match _cache.ContainsKey templatePath with + | true -> () + | false -> + let! file = File.ReadAllTextAsync $"{templatePath}.liquid" + _cache[templatePath] <- Template.Parse (file, SyntaxCompatibility.DotLiquid22) + return _cache[templatePath] + } + diff --git a/src/MyWebLog/Handlers.fs b/src/MyWebLog/Handlers.fs index 8addda5..0e6b86e 100644 --- a/src/MyWebLog/Handlers.fs +++ b/src/MyWebLog/Handlers.fs @@ -1,7 +1,6 @@ [] module MyWebLog.Handlers -open System.Collections.Generic open DotLiquid open Giraffe open Microsoft.AspNetCore.Http @@ -26,12 +25,11 @@ module Error = >=> text ex.Message *) /// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response - let notAuthorized : HttpHandler = - fun next ctx -> - (next, ctx) - ||> match ctx.Request.Method with - | "GET" -> redirectTo false $"/user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}" - | _ -> setStatusCode 401 >=> fun _ _ -> Task.FromResult None + let notAuthorized : HttpHandler = fun next ctx -> + (next, ctx) + ||> match ctx.Request.Method with + | "GET" -> redirectTo false $"/user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}" + | _ -> setStatusCode 401 >=> fun _ _ -> Task.FromResult None /// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there let notFound : HttpHandler = @@ -41,42 +39,27 @@ module Error = [] module private Helpers = + open Markdig open Microsoft.AspNetCore.Antiforgery open Microsoft.Extensions.DependencyInjection - open System.Collections.Concurrent - open System.IO - - /// Cache for parsed templates - module private TemplateCache = - - /// Cache of parsed templates - let private views = ConcurrentDictionary () - - /// Get a template for the given web log - let get (theme : string) (templateName : string) = task { - let templatePath = $"themes/{theme}/{templateName}" - match views.ContainsKey templatePath with - | true -> () - | false -> - let! file = File.ReadAllTextAsync $"{templatePath}.liquid" - views[templatePath] <- Template.Parse (file, SyntaxCompatibility.DotLiquid22) - return views[templatePath] - } - + open System.Security.Claims + /// Either get the web log from the hash, or get it from the cache and add it to the hash - let deriveWebLogFromHash (hash : Hash) ctx = + let private deriveWebLogFromHash (hash : Hash) ctx = match hash.ContainsKey "web_log" with | true -> hash["web_log"] :?> WebLog | false -> - let wl = WebLogCache.getByCtx ctx + let wl = WebLogCache.get ctx hash.Add ("web_log", wl) wl /// Render a view for the specified theme, using the specified template, layout, and hash - let viewForTheme theme template layout next ctx = fun (hash : Hash) -> task { + let viewForTheme theme template next ctx = fun (hash : Hash) -> task { // Don't need the web log, but this adds it to the hash if the function is called directly let _ = deriveWebLogFromHash hash ctx - hash.Add ("logged_on", ctx.User.Identity.IsAuthenticated) + hash.Add ("logged_on", ctx.User.Identity.IsAuthenticated) + hash.Add ("page_list", PageListCache.get ctx) + hash.Add ("current_page", ctx.Request.Path.Value.Substring 1) // NOTE: DotLiquid does not support {% render %} or {% include %} in its templates, so we will do a two-pass // render; the net effect is a "layout" capability similar to Razor or Pug @@ -86,20 +69,26 @@ module private Helpers = hash.Add ("content", contentTemplate.Render hash) // ...then render that content with its layout - let! layoutTemplate = TemplateCache.get theme (defaultArg layout "layout") + let! layoutTemplate = TemplateCache.get theme "layout" return! htmlString (layoutTemplate.Render hash) next ctx } /// Return a view for the web log's default theme - let themedView template layout next ctx = fun (hash : Hash) -> task { - return! viewForTheme (deriveWebLogFromHash hash ctx).themePath template layout next ctx hash + let themedView template next ctx = fun (hash : Hash) -> task { + return! viewForTheme (deriveWebLogFromHash hash ctx).themePath template next ctx hash } - /// The web log ID for the current request - let webLogId ctx = (WebLogCache.getByCtx ctx).id + /// Get the web log ID for the current request + let webLogId ctx = (WebLogCache.get ctx).id + /// Get the user ID for the current request + let userId (ctx : HttpContext) = + WebLogUserId (ctx.User.Claims |> Seq.find (fun c -> c.Type = ClaimTypes.NameIdentifier)).Value + + /// Get the RethinkDB connection let conn (ctx : HttpContext) = ctx.RequestServices.GetRequiredService () + /// Get the Anti-CSRF service let private antiForgery (ctx : HttpContext) = ctx.RequestServices.GetRequiredService () /// Get the cross-site request forgery token set @@ -115,16 +104,26 @@ module private Helpers = /// Require a user to be logged on let requireUser = requiresAuthentication Error.notAuthorized + + /// Pipeline with most extensions enabled + let mdPipeline = + MarkdownPipelineBuilder().UseSmartyPants().UseAdvancedExtensions().Build () + + /// Get the HTML representation of the text of a revision + let revisionToHtml (rev : Revision) = + match rev.sourceType with Html -> rev.text | Markdown -> Markdown.ToHtml (rev.text, mdPipeline) +open System.Collections.Generic + /// Handlers to manipulate admin functions module Admin = - // GET /admin/ + // GET /admin let dashboard : HttpHandler = requireUser >=> fun next ctx -> task { - let webLogId' = webLogId ctx - let conn' = conn ctx - let getCount (f : WebLogId -> IConnection -> Task) = f webLogId' conn' + let webLogId = webLogId ctx + let conn = conn ctx + let getCount (f : WebLogId -> IConnection -> Task) = f webLogId conn let! posts = Data.Post.countByStatus Published |> getCount let! drafts = Data.Post.countByStatus Draft |> getCount let! pages = Data.Page.countAll |> getCount @@ -143,12 +142,12 @@ module Admin = topLevelCategories = topCats } |} - |> viewForTheme "admin" "dashboard" None next ctx + |> viewForTheme "admin" "dashboard" next ctx } // GET /admin/settings let settings : HttpHandler = requireUser >=> fun next ctx -> task { - let webLog = WebLogCache.getByCtx ctx + let webLog = WebLogCache.get ctx let! allPages = Data.Page.findAll webLog.id (conn ctx) return! Hash.FromAnonymousObject @@ -170,14 +169,14 @@ module Admin = web_log = webLog page_title = "Web Log Settings" |} - |> viewForTheme "admin" "settings" None next ctx + |> viewForTheme "admin" "settings" next ctx } // POST /admin/settings let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { - let conn' = conn ctx + let conn = conn ctx let! model = ctx.BindFormAsync () - match! Data.WebLog.findByHost (WebLogCache.getByCtx ctx).urlBase conn' with + match! Data.WebLog.findById (WebLogCache.get ctx).id conn with | Some webLog -> let updated = { webLog with @@ -187,14 +186,102 @@ module Admin = postsPerPage = model.postsPerPage timeZone = model.timeZone } - do! Data.WebLog.updateSettings updated conn' + do! Data.WebLog.updateSettings updated conn // Update cache - WebLogCache.set updated.urlBase updated + WebLogCache.set ctx updated // TODO: confirmation message - return! redirectTo false "/admin/" next ctx + return! redirectTo false "/admin" next ctx + | None -> return! Error.notFound next ctx + } + + +/// Handlers to manipulate pages +module Page = + + // GET /pages + // GET /pages/page/{pageNbr} + let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { + let webLog = WebLogCache.get ctx + let! pages = Data.Page.findPageOfPages webLog.id pageNbr (conn ctx) + return! + Hash.FromAnonymousObject + {| pages = pages |> List.map (DisplayPage.fromPage webLog) + page_title = "Pages" + |} + |> viewForTheme "admin" "page-list" next ctx + } + + // GET /page/{id}/edit + let edit pgId : HttpHandler = requireUser >=> fun next ctx -> task { + let! hash = task { + match pgId with + | "new" -> + return + Hash.FromAnonymousObject {| + csrf = csrfToken ctx + model = EditPageModel.fromPage { Page.empty with id = PageId "new" } + page_title = "Add a New Page" + |} |> Some + | _ -> + match! Data.Page.findByFullId (PageId pgId) (webLogId ctx) (conn ctx) with + | Some page -> + return + Hash.FromAnonymousObject {| + csrf = csrfToken ctx + model = EditPageModel.fromPage page + page_title = "Edit Page" + |} |> Some + | None -> return None + } + match hash with + | Some h -> return! viewForTheme "admin" "page-edit" next ctx h + | None -> return! Error.notFound next ctx + } + + // POST /page/{id}/edit + let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { + let! model = ctx.BindFormAsync () + let webLogId = webLogId ctx + let conn = conn ctx + let now = DateTime.UtcNow + let! pg = task { + match model.pageId with + | "new" -> + return Some + { Page.empty with + id = PageId.create () + webLogId = webLogId + authorId = userId ctx + publishedOn = now + } + | pgId -> return! Data.Page.findByFullId (PageId pgId) webLogId conn + } + match pg with + | Some page -> + let updateList = page.showInPageList <> model.isShownInPageList + let revision = { asOf = now; sourceType = RevisionSource.ofString model.source; text = model.text } + // Detect a permalink change, and add the prior one to the prior list + let page = + match Permalink.toString page.permalink with + | "" -> page + | link when link = model.permalink -> page + | _ -> { page with priorPermalinks = page.permalink :: page.priorPermalinks } + let page = + { page with + title = model.title + permalink = Permalink model.permalink + updatedOn = now + showInPageList = model.isShownInPageList + text = revisionToHtml revision + revisions = revision :: page.revisions + } + do! (match model.pageId with "new" -> Data.Page.add | _ -> Data.Page.update) page conn + if updateList then do! PageListCache.update ctx + // TODO: confirmation + return! redirectTo false $"/page/{PageId.toString page.id}/edit" next ctx | None -> return! Error.notFound next ctx } @@ -204,21 +291,21 @@ module Post = // GET /page/{pageNbr} let pageOfPosts (pageNbr : int) : HttpHandler = fun next ctx -> task { - let webLog = WebLogCache.getByCtx ctx - let! posts = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage (conn ctx) - let hash = Hash.FromAnonymousObject {| posts = posts |} - let title = + let webLog = WebLogCache.get ctx + let! posts = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage (conn ctx) + let hash = Hash.FromAnonymousObject {| posts = posts |} + let title = match pageNbr, webLog.defaultPage with | 1, "posts" -> None | _, "posts" -> Some $"Page {pageNbr}" | _, _ -> Some $"Page {pageNbr} « Posts" match title with Some ttl -> hash.Add ("page_title", ttl) | None -> () - return! themedView "index" None next ctx hash + return! themedView "index" next ctx hash } // GET / let home : HttpHandler = fun next ctx -> task { - let webLog = WebLogCache.getByCtx ctx + let webLog = WebLogCache.get ctx match webLog.defaultPage with | "posts" -> return! pageOfPosts 1 next ctx | pageId -> @@ -226,31 +313,38 @@ module Post = | Some page -> return! Hash.FromAnonymousObject {| page = page; page_title = page.title |} - |> themedView "single-page" page.template next ctx + |> themedView (defaultArg page.template "single-page") next ctx | None -> return! Error.notFound next ctx } - // GET * - let catchAll (link : string) : HttpHandler = fun next ctx -> task { - let webLog = WebLogCache.getByCtx ctx - let conn' = conn ctx - let permalink = Permalink link - match! Data.Post.findByPermalink permalink webLog.id conn' with - | Some post -> return! Error.notFound next ctx + // GET {**link} + let catchAll : HttpHandler = fun next ctx -> task { + let webLog = WebLogCache.get ctx + let conn = conn ctx + let permalink = (string >> Permalink) ctx.Request.RouteValues["link"] + // Current post + match! Data.Post.findByPermalink permalink webLog.id conn with + | Some _ -> return! Error.notFound next ctx // TODO: return via single-post action | None -> - match! Data.Page.findByPermalink permalink webLog.id conn' with + // Current page + match! Data.Page.findByPermalink permalink webLog.id conn with | Some page -> return! Hash.FromAnonymousObject {| page = page; page_title = page.title |} - |> themedView "single-page" page.template next ctx + |> themedView (defaultArg page.template "single-page") next ctx | None -> - - // TOOD: search prior permalinks for posts and pages - - // We tried, we really tried... - Console.Write($"Returning 404 for permalink |{permalink}|"); - return! Error.notFound next ctx + // Prior post + match! Data.Post.findCurrentPermalink permalink webLog.id conn with + | Some link -> return! redirectTo true $"/{Permalink.toString link}" next ctx + | None -> + // Prior page + match! Data.Page.findCurrentPermalink permalink webLog.id conn with + | Some link -> return! redirectTo true $"/{Permalink.toString link}" next ctx + | None -> + // We tried, we really did... + Console.Write($"Returning 404 for permalink |{permalink}|"); + return! Error.notFound next ctx } @@ -265,15 +359,15 @@ module User = /// Hash a password for a given user let hashedPassword (plainText : string) (email : string) (salt : Guid) = - let allSalt = Array.concat [ salt.ToByteArray(); (Encoding.UTF8.GetBytes email) ] - use alg = new Rfc2898DeriveBytes (plainText, allSalt, 2_048) - Convert.ToBase64String(alg.GetBytes(64)) + let allSalt = Array.concat [ salt.ToByteArray (); Encoding.UTF8.GetBytes email ] + use alg = new Rfc2898DeriveBytes (plainText, allSalt, 2_048) + Convert.ToBase64String (alg.GetBytes 64) // GET /user/log-on let logOn : HttpHandler = fun next ctx -> task { return! Hash.FromAnonymousObject {| page_title = "Log On"; csrf = (csrfToken ctx) |} - |> viewForTheme "admin" "log-on" None next ctx + |> viewForTheme "admin" "log-on" next ctx } // POST /user/log-on @@ -283,9 +377,9 @@ module User = | Some user when user.passwordHash = hashedPassword model.password user.userName user.salt -> let claims = seq { Claim (ClaimTypes.NameIdentifier, WebLogUserId.toString user.id) - Claim (ClaimTypes.Name, $"{user.firstName} {user.lastName}") - Claim (ClaimTypes.GivenName, user.preferredName) - Claim (ClaimTypes.Role, user.authorizationLevel.ToString ()) + Claim (ClaimTypes.Name, $"{user.firstName} {user.lastName}") + Claim (ClaimTypes.GivenName, user.preferredName) + Claim (ClaimTypes.Role, user.authorizationLevel.ToString ()) } let identity = ClaimsIdentity (claims, CookieAuthenticationDefaults.AuthenticationScheme) @@ -294,7 +388,7 @@ module User = // TODO: confirmation message - return! redirectTo false "/admin/" next ctx + return! redirectTo false "/admin" next ctx | _ -> // TODO: make error, not 404 return! Error.notFound next ctx @@ -318,7 +412,7 @@ let endpoints = [ ] subRoute "/admin" [ GET [ - route "/" Admin.dashboard + route "" Admin.dashboard route "/settings" Admin.settings ] POST [ @@ -327,7 +421,13 @@ let endpoints = [ ] subRoute "/page" [ GET [ - routef "/%d" Post.pageOfPosts + routef "/%d" Post.pageOfPosts + routef "/%s/edit" Page.edit + route "s" (Page.all 1) + routef "s/page/%d" Page.all + ] + POST [ + route "/save" Page.save ] ] subRoute "/user" [ @@ -339,4 +439,5 @@ let endpoints = [ route "/log-on" User.doLogOn ] ] + route "{**link}" Post.catchAll ] diff --git a/src/MyWebLog/MyWebLog.fsproj b/src/MyWebLog/MyWebLog.fsproj index 8344369..ddb2839 100644 --- a/src/MyWebLog/MyWebLog.fsproj +++ b/src/MyWebLog/MyWebLog.fsproj @@ -3,11 +3,12 @@ Exe net6.0 + 3391 - + @@ -15,6 +16,7 @@ + diff --git a/src/MyWebLog/Program.fs b/src/MyWebLog/Program.fs index 6f8a23f..c7f6554 100644 --- a/src/MyWebLog/Program.fs +++ b/src/MyWebLog/Program.fs @@ -9,19 +9,57 @@ open System type WebLogMiddleware (next : RequestDelegate) = member this.InvokeAsync (ctx : HttpContext) = task { - let host = ctx.Request.Host.ToUriComponent () - match WebLogCache.exists host with + match WebLogCache.exists ctx with | true -> return! next.Invoke ctx | false -> let conn = ctx.RequestServices.GetRequiredService () - match! Data.WebLog.findByHost host conn with + match! Data.WebLog.findByHost (Cache.makeKey ctx) conn with | Some webLog -> - WebLogCache.set host webLog + WebLogCache.set ctx webLog + do! PageListCache.update ctx return! next.Invoke ctx | None -> ctx.Response.StatusCode <- 404 } +/// DotLiquid filters +module DotLiquidBespoke = + + open DotLiquid + open System.IO + + /// A filter to generate nav links, highlighting the active link (exact match) + type NavLinkFilter () = + static member NavLink (ctx : Context, url : string, text : string) = + seq { + "
  • " + text + "
  • " + } + |> Seq.fold (+) "" + + /// Create links for a user to log on or off, and a dashboard link if they are logged off + type UserLinksTag () = + inherit Tag () + + override this.Render (context : Context, result : TextWriter) = + seq { + """" + } + |> Seq.iter result.WriteLine + + /// Initialize a new database let initDbValidated (args : string[]) (sp : IServiceProvider) = task { @@ -140,14 +178,20 @@ let main args = let _ = builder.Services.AddSingleton conn // Set up DotLiquid + Template.RegisterFilter typeof + Template.RegisterTag "user_links" + let all = [| "*" |] Template.RegisterSafeType (typeof, all) Template.RegisterSafeType (typeof, all) + Template.RegisterSafeType (typeof, all) + Template.RegisterSafeType (typeof, all) Template.RegisterSafeType (typeof, all) + Template.RegisterSafeType (typeof, all) Template.RegisterSafeType (typeof, all) - Template.RegisterSafeType (typeof>, all) // doesn't quite get the job done.... + Template.RegisterSafeType (typeof, all) Template.RegisterSafeType (typeof, all) let app = builder.Build () @@ -160,7 +204,7 @@ let main args = let _ = app.UseAuthentication () let _ = app.UseStaticFiles () let _ = app.UseRouting () - let _ = app.UseEndpoints (fun endpoints -> endpoints.MapGiraffeEndpoints Handlers.endpoints) + let _ = app.UseGiraffe Handlers.endpoints app.Run() diff --git a/src/MyWebLog/WebLogCache.fs b/src/MyWebLog/WebLogCache.fs deleted file mode 100644 index 8c03d21..0000000 --- a/src/MyWebLog/WebLogCache.fs +++ /dev/null @@ -1,24 +0,0 @@ -/// -/// In-memory cache of web log details -/// -/// This is filled by the middleware via the first request for each host, and can be updated via the web log -/// settings update page -module MyWebLog.WebLogCache - -open Microsoft.AspNetCore.Http -open System.Collections.Concurrent - -/// The cache of web log details -let private _cache = ConcurrentDictionary () - -/// Does a host exist in the cache? -let exists host = _cache.ContainsKey host - -/// Get the details for a web log via its host -let get host = _cache[host] - -/// Get the details for a web log via its host -let getByCtx (ctx : HttpContext) = _cache[ctx.Request.Host.ToUriComponent ()] - -/// Set the details for a particular host -let set host details = _cache[host] <- details diff --git a/src/MyWebLog/themes/admin/dashboard.liquid b/src/MyWebLog/themes/admin/dashboard.liquid index 7347437..1c62b29 100644 --- a/src/MyWebLog/themes/admin/dashboard.liquid +++ b/src/MyWebLog/themes/admin/dashboard.liquid @@ -1,4 +1,5 @@ -
    +

    {{ web_log.name }} • Dashboard

    +
    @@ -21,7 +22,7 @@ All {{ model.pages }}   Shown in Page List {{ model.listed_pages }} - View All + View All Create a New Page
    diff --git a/src/MyWebLog/themes/admin/layout.liquid b/src/MyWebLog/themes/admin/layout.liquid index 88e2929..8672d52 100644 --- a/src/MyWebLog/themes/admin/layout.liquid +++ b/src/MyWebLog/themes/admin/layout.liquid @@ -17,20 +17,26 @@ -
    +
    {{ content }}