WIP on DotLiquid support
This commit is contained in:
1
src/MyWebLog/Db/.gitignore
vendored
1
src/MyWebLog/Db/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*.db*
|
||||
@@ -1,53 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace MyWebLog.Features.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for admin-specific displays and routes
|
||||
/// </summary>
|
||||
[Route("/admin")]
|
||||
[Authorize]
|
||||
public class AdminController : MyWebLogController
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public AdminController(WebLogDbContext db) : base(db) { }
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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));
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
namespace MyWebLog.Features.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// The model used to display the dashboard
|
||||
/// </summary>
|
||||
public class DashboardModel : MyWebLogModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of published posts
|
||||
/// </summary>
|
||||
public int Posts { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The number of post drafts
|
||||
/// </summary>
|
||||
public int Drafts { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The number of pages
|
||||
/// </summary>
|
||||
public int Pages { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The number of pages in the page list
|
||||
/// </summary>
|
||||
public int ListedPages { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The number of categories
|
||||
/// </summary>
|
||||
public int Categories { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The top-level categories
|
||||
/// </summary>
|
||||
public int TopLevelCategories { get; set; } = 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DashboardModel(WebLogDetails webLog) : base(webLog) { }
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
@model DashboardModel
|
||||
@{
|
||||
Layout = "_AdminLayout";
|
||||
ViewBag.Title = Resources.Dashboard;
|
||||
}
|
||||
<article class="container pt-3">
|
||||
<div class="row">
|
||||
<section class="col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3">
|
||||
<div class="card">
|
||||
<header class="card-header text-white bg-primary">@Resources.Posts</header>
|
||||
<div class="card-body">
|
||||
<h6 class="card-subtitle text-muted pb-3">
|
||||
@Resources.Published <span class="badge rounded-pill bg-secondary">@Model.Posts</span>
|
||||
@Resources.Drafts <span class="badge rounded-pill bg-secondary">@Model.Drafts</span>
|
||||
</h6>
|
||||
<a asp-action="All" asp-controller="Post" class="btn btn-secondary me-2">@Resources.ViewAll</a>
|
||||
<a asp-action="Edit" asp-controller="Post" asp-route-id="new" class="btn btn-primary">
|
||||
@Resources.WriteANewPost
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="col-lg-5 col-xl-4 pb-3">
|
||||
<div class="card">
|
||||
<header class="card-header text-white bg-primary">@Resources.Pages</header>
|
||||
<div class="card-body">
|
||||
<h6 class="card-subtitle text-muted pb-3">
|
||||
@Resources.All <span class="badge rounded-pill bg-secondary">@Model.Pages</span>
|
||||
@Resources.ShownInPageList <span class="badge rounded-pill bg-secondary">@Model.ListedPages</span>
|
||||
</h6>
|
||||
<a asp-action="All" asp-controller="Page" class="btn btn-secondary me-2">@Resources.ViewAll</a>
|
||||
<a asp-action="Edit" asp-controller="Page" asp-route-id="new" class="btn btn-primary">
|
||||
@Resources.CreateANewPage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="row">
|
||||
<section class="col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3">
|
||||
<div class="card">
|
||||
<header class="card-header text-white bg-secondary">@Resources.Categories</header>
|
||||
<div class="card-body">
|
||||
<h6 class="card-subtitle text-muted pb-3">
|
||||
@Resources.All <span class="badge rounded-pill bg-secondary">@Model.Categories</span>
|
||||
@Resources.TopLevel <span class="badge rounded-pill bg-secondary">@Model.TopLevelCategories</span>
|
||||
</h6>
|
||||
<a asp-action="All" asp-controller="Category" class="btn btn-secondary me-2">@Resources.ViewAll</a>
|
||||
<a asp-action="Edit" asp-controller="Category" asp-route-id="new" class="btn btn-secondary">
|
||||
@Resources.AddANewCategory
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col text-end">
|
||||
<a asp-action="Settings" class="btn btn-secondary">@Resources.ModifySettings</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1,50 +0,0 @@
|
||||
@model SettingsModel
|
||||
@{
|
||||
Layout = "_AdminLayout";
|
||||
ViewBag.Title = Resources.WebLogSettings;
|
||||
}
|
||||
<article class="pt-3">
|
||||
<form asp-action="SaveSettings" method="post">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-xl-4 offset-xl-2 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" asp-for="Name" class="form-control">
|
||||
<label asp-for="Name"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" asp-for="Subtitle" class="form-control">
|
||||
<label asp-for="Subtitle"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 col-xl-2 offset-xl-2 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="number" asp-for="PostsPerPage" class="form-control" min="0" max="50">
|
||||
<label asp-for="PostsPerPage"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-3 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" asp-for="TimeZone" class="form-control">
|
||||
<label asp-for="TimeZone"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-3 pb-3">
|
||||
<div class="form-floating">
|
||||
<select asp-for="DefaultPage" asp-items="Model.DefaultPages" class="form-control"></select>
|
||||
<label asp-for="DefaultPage"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col text-center">
|
||||
<button type="submit" class="btn btn-primary">@Resources.SaveChanges</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
@@ -1,76 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MyWebLog.Features.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// View model for editing web log settings
|
||||
/// </summary>
|
||||
public class SettingsModel : MyWebLogModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the web log
|
||||
/// </summary>
|
||||
[Required(AllowEmptyStrings = false)]
|
||||
[Display(ResourceType = typeof(Resources), Name = "Name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The subtitle of the web log
|
||||
/// </summary>
|
||||
[Display(ResourceType = typeof(Resources), Name = "Subtitle")]
|
||||
public string Subtitle { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The default page
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Display(ResourceType = typeof(Resources), Name = "DefaultPage")]
|
||||
public string DefaultPage { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// How many posts should appear on index pages
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Display(ResourceType = typeof(Resources), Name = "PostsPerPage")]
|
||||
[Range(0, 50)]
|
||||
public byte PostsPerPage { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// The time zone in which dates/times should be displayed
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Display(ResourceType = typeof(Resources), Name = "TimeZone")]
|
||||
public string TimeZone { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Possible values for the default page
|
||||
/// </summary>
|
||||
public IEnumerable<SelectListItem> DefaultPages { get; set; } = Enumerable.Empty<SelectListItem>();
|
||||
|
||||
[Obsolete("Only used for model binding; use the WebLogDetails constructor")]
|
||||
public SettingsModel() : base(new()) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public SettingsModel(WebLogDetails webLog) : base(webLog)
|
||||
{
|
||||
Name = webLog.Name;
|
||||
Subtitle = webLog.Subtitle ?? "";
|
||||
DefaultPage = webLog.DefaultPage;
|
||||
PostsPerPage = webLog.PostsPerPage;
|
||||
TimeZone = webLog.TimeZone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate the settings object from the data in this form
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings to be updated</param>
|
||||
public void PopulateSettings(WebLogDetails settings)
|
||||
{
|
||||
settings.Name = Name;
|
||||
settings.Subtitle = Subtitle == "" ? null : Subtitle;
|
||||
settings.DefaultPage = DefaultPage;
|
||||
settings.PostsPerPage = PostsPerPage;
|
||||
settings.TimeZone = TimeZone;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace MyWebLog.Features.Categories;
|
||||
|
||||
/// <summary>
|
||||
/// Handle routes for categories
|
||||
/// </summary>
|
||||
[Route("/category")]
|
||||
[Authorize]
|
||||
public class CategoryController : MyWebLogController
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public CategoryController(WebLogDbContext db) : base(db) { }
|
||||
|
||||
[HttpGet("all")]
|
||||
public async Task<IActionResult> All()
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/edit")]
|
||||
public async Task<IActionResult> Edit(string id)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A controller model convention that identifies the feature in which a controller exists
|
||||
/// </summary>
|
||||
public class FeatureControllerModelConvention : IControllerModelConvention
|
||||
{
|
||||
/// <summary>
|
||||
/// A cache of controller types to features
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, string> _features = new();
|
||||
|
||||
/// <summary>
|
||||
/// Derive the feature name from the controller's type
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(ControllerModel controller) =>
|
||||
controller.Properties.Add("feature", GetFeatureName(controller.ControllerType));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expand the location token with the feature name
|
||||
/// </summary>
|
||||
public class FeatureViewLocationExpander : IViewLocationExpander
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
||||
IEnumerable<string> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PopulateValues(ViewLocationExpanderContext _) { }
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
@model PageListModel
|
||||
@{
|
||||
Layout = "_AdminLayout";
|
||||
ViewBag.Title = Resources.Pages;
|
||||
}
|
||||
<article class="container">
|
||||
<a asp-action="Edit" asp-route-id="new" class="btn btn-primary btn-sm my-3">@Resources.CreateANewPage</a>
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">@Resources.Actions</th>
|
||||
<th scope="col">@Resources.Title</th>
|
||||
<th scope="col">@Resources.InListQuestion</th>
|
||||
<th scope="col">@Resources.LastUpdated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var pg in Model.Pages)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="Edit" asp-route-id="@pg.Id">@Resources.Edit</a>
|
||||
</td>
|
||||
<td>
|
||||
@pg.Title
|
||||
@if (pg.Id == Model.WebLog.DefaultPage)
|
||||
{
|
||||
<text> </text><span class="badge bg-success">HOME PAGE</span>
|
||||
}
|
||||
</td>
|
||||
<td><yes-no asp-for="pg.ShowInPageList" /></td>
|
||||
<td>@pg.UpdatedOn.ToString(Resources.DateFormatString)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
@@ -1,56 +0,0 @@
|
||||
@model EditPageModel
|
||||
@{
|
||||
Layout = "_AdminLayout";
|
||||
ViewBag.Title = Model.IsNew ? Resources.AddANewPage : Resources.EditPage;
|
||||
}
|
||||
<article>
|
||||
<h2 class="pb-3">@ViewBag.Title</h2>
|
||||
<form asp-action="Save" asp-route-id="@Model.PageId" method="post">
|
||||
<input type="hidden" asp-for="PageId">
|
||||
<div class="container">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div class="form-floating">
|
||||
<input type="text" asp-for="Title" class="form-control" autofocus>
|
||||
<label asp-for="Title"></label>
|
||||
<span asp-validation-for="Title"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-9">
|
||||
<div class="form-floating">
|
||||
<input type="text" asp-for="Permalink" class="form-control">
|
||||
<label asp-for="Permalink"></label>
|
||||
<span asp-validation-for="Permalink"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 align-self-center">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" asp-for="IsShownInPageList" class="form-check-input">
|
||||
<label asp-for="IsShownInPageList" class="form-check-label"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<label asp-for="Text"></label>
|
||||
<input type="radio" asp-for="Source" id="source_html" class="btn-check" value="@RevisionSource.Html">
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
|
||||
<input type="radio" asp-for="Source" id="source_md" class="btn-check" value="@RevisionSource.Markdown">
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<textarea asp-for="Text" class="form-control" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<button type="submit" class="btn btn-primary">@Resources.SaveChanges</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
@@ -1,99 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MyWebLog.Features.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// Model used to edit pages
|
||||
/// </summary>
|
||||
public class EditPageModel : MyWebLogModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the page being edited
|
||||
/// </summary>
|
||||
public string PageId { get; set; } = "new";
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is a new page
|
||||
/// </summary>
|
||||
public bool IsNew => PageId == "new";
|
||||
|
||||
/// <summary>
|
||||
/// The title of the page
|
||||
/// </summary>
|
||||
[Display(ResourceType = typeof(Resources), Name = "Title")]
|
||||
[Required(AllowEmptyStrings = false)]
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The permalink for the page
|
||||
/// </summary>
|
||||
[Display(ResourceType = typeof(Resources), Name = "Permalink")]
|
||||
[Required(AllowEmptyStrings = false)]
|
||||
public string Permalink { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Whether this page is shown in the page list
|
||||
/// </summary>
|
||||
[Display(ResourceType = typeof(Resources), Name = "ShowInPageList")]
|
||||
public bool IsShownInPageList { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The source format for the text
|
||||
/// </summary>
|
||||
public RevisionSource Source { get; set; } = RevisionSource.Html;
|
||||
|
||||
/// <summary>
|
||||
/// The text of the page
|
||||
/// </summary>
|
||||
[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()) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public EditPageModel(WebLogDetails webLog) : base(webLog) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a model from an existing page
|
||||
/// </summary>
|
||||
/// <param name="page">The page from which the model will be created</param>
|
||||
/// <param name="webLog">The web log to which the page belongs</param>
|
||||
/// <returns>A populated model</returns>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate a page from the values contained in this page
|
||||
/// </summary>
|
||||
/// <param name="page">The page to be populated</param>
|
||||
/// <returns>The populated page</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using Markdig;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace MyWebLog.Features.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// Handle routes for pages
|
||||
/// </summary>
|
||||
[Route("/page")]
|
||||
[Authorize]
|
||||
public class PageController : MyWebLogController
|
||||
{
|
||||
/// <summary>
|
||||
/// Pipeline with most extensions enabled
|
||||
/// </summary>
|
||||
private readonly MarkdownPipeline _pipeline = new MarkdownPipelineBuilder()
|
||||
.UseSmartyPants().UseAdvancedExtensions().Build();
|
||||
|
||||
/// <inheritdoc />
|
||||
public PageController(WebLogDbContext db) : base(db) { }
|
||||
|
||||
[HttpGet("all")]
|
||||
[HttpGet("all/page/{pageNbr:int}")]
|
||||
public async Task<IActionResult> All(int? pageNbr) =>
|
||||
View(new PageListModel(await Db.Pages.FindPageOfPages(pageNbr ?? 1), WebLog));
|
||||
|
||||
[HttpGet("{id}/edit")]
|
||||
public async Task<IActionResult> 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<IActionResult> Save(EditPageModel model)
|
||||
{
|
||||
var page = model.PopulatePage(model.IsNew
|
||||
? new()
|
||||
{
|
||||
Id = WebLogDbContext.NewId(),
|
||||
AuthorId = UserId,
|
||||
PublishedOn = DateTime.UtcNow,
|
||||
Revisions = new List<PageRevision>()
|
||||
}
|
||||
: 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));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace MyWebLog.Features.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// View model for viewing a list of pages
|
||||
/// </summary>
|
||||
public class PageListModel : MyWebLogModel
|
||||
{
|
||||
public IList<Page> Pages { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="pages">The pages to display</param>
|
||||
/// <param name="webLog">The web log details</param>
|
||||
public PageListModel(IList<Page> pages, WebLogDetails webLog) : base(webLog)
|
||||
{
|
||||
Pages = pages;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace MyWebLog.Features.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// The model used to render a single page
|
||||
/// </summary>
|
||||
public class SinglePageModel : MyWebLogModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The page to be rendered
|
||||
/// </summary>
|
||||
public Page Page { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this the home page?
|
||||
/// </summary>
|
||||
public bool IsHome => Page.Id == WebLog.DefaultPage;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="page">The page to be rendered</param>
|
||||
/// <param name="webLog">The details for the web log</param>
|
||||
public SinglePageModel(Page page, WebLogDetails webLog) : base(webLog)
|
||||
{
|
||||
Page = page;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace MyWebLog.Features.Posts;
|
||||
|
||||
/// <summary>
|
||||
/// The model used to render multiple posts
|
||||
/// </summary>
|
||||
public class MultiplePostModel : MyWebLogModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The posts to be rendered
|
||||
/// </summary>
|
||||
public IEnumerable<Post> Posts { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="posts">The posts to be rendered</param>
|
||||
/// <param name="webLog">The details for the web log</param>
|
||||
public MultiplePostModel(IEnumerable<Post> posts, WebLogDetails webLog) : base(webLog)
|
||||
{
|
||||
Posts = posts;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MyWebLog.Features.Pages;
|
||||
|
||||
namespace MyWebLog.Features.Posts;
|
||||
|
||||
/// <summary>
|
||||
/// Handle post-related requests
|
||||
/// </summary>
|
||||
[Route("/post")]
|
||||
[Authorize]
|
||||
public class PostController : MyWebLogController
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public PostController(WebLogDbContext db) : base(db) { }
|
||||
|
||||
[HttpGet("~/")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> 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<IActionResult> PageOfPosts(int pageNbr) =>
|
||||
ThemedView("Index",
|
||||
new MultiplePostModel(await Db.Posts.FindPageOfPublishedPosts(pageNbr, WebLog.PostsPerPage), WebLog));
|
||||
|
||||
[HttpGet("~/{*permalink}")]
|
||||
public async Task<IActionResult> 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<IActionResult> All()
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/edit")]
|
||||
public async Task<IActionResult> Edit(string id)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace MyWebLog.Features.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for myWebLog controllers
|
||||
/// </summary>
|
||||
public abstract class MyWebLogController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// The data context to use to fulfil this request
|
||||
/// </summary>
|
||||
protected WebLogDbContext Db { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The details for the current web log
|
||||
/// </summary>
|
||||
protected WebLogDetails WebLog => WebLogCache.Get(HttpContext);
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the currently authenticated user
|
||||
/// </summary>
|
||||
protected string UserId => User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "";
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="db">The data context to use to fulfil this request</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
namespace MyWebLog.Features.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Base model class for myWebLog views
|
||||
/// </summary>
|
||||
public class MyWebLogModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The details for the web log
|
||||
/// </summary>
|
||||
public WebLogDetails WebLog { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="webLog">The details for the web log</param>
|
||||
protected MyWebLogModel(WebLogDetails webLog)
|
||||
{
|
||||
WebLog = webLog;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Image tag helper to load a theme's image
|
||||
/// </summary>
|
||||
[HtmlTargetElement("img", Attributes = "asp-theme")]
|
||||
public class ImageTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.ImageTagHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The theme for which the image should be loaded
|
||||
/// </summary>
|
||||
[HtmlAttributeName("asp-theme")]
|
||||
public string Theme { get; set; } = "";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageTagHelper(IFileVersionProvider fileVersionProvider, HtmlEncoder htmlEncoder,
|
||||
IUrlHelperFactory urlHelperFactory)
|
||||
: base(fileVersionProvider, htmlEncoder, urlHelperFactory) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tag helper to link stylesheets for a theme
|
||||
/// </summary>
|
||||
[HtmlTargetElement("link", Attributes = "asp-theme")]
|
||||
public class LinkTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.LinkTagHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The theme for which a style sheet should be loaded
|
||||
/// </summary>
|
||||
[HtmlAttributeName("asp-theme")]
|
||||
public string Theme { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The style sheet to be loaded (defaults to "style")
|
||||
/// </summary>
|
||||
[HtmlAttributeName("asp-style")]
|
||||
public string Style { get; set; } = "style";
|
||||
|
||||
/// <inheritdoc />
|
||||
public LinkTagHelper(IWebHostEnvironment hostingEnvironment, TagHelperMemoryCacheProvider cacheProvider,
|
||||
IFileVersionProvider fileVersionProvider, HtmlEncoder htmlEncoder, JavaScriptEncoder javaScriptEncoder,
|
||||
IUrlHelperFactory urlHelperFactory)
|
||||
: base(hostingEnvironment, cacheProvider, fileVersionProvider, htmlEncoder, javaScriptEncoder, urlHelperFactory)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
namespace MyWebLog.Features.Shared.TagHelpers;
|
||||
|
||||
/// <summary>
|
||||
/// Write a Yes or No based on a boolean value
|
||||
/// </summary>
|
||||
public class YesNoTagHelper : TagHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The attribute in question
|
||||
/// </summary>
|
||||
[HtmlAttributeName("asp-for")]
|
||||
public bool For { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional; if set, that value will be wrapped with <strong> instead of <span>
|
||||
/// </summary>
|
||||
[HtmlAttributeName("asp-strong-if")]
|
||||
public bool? StrongIf { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
@model MyWebLogModel
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>@ViewBag.Title « @Resources.Admin « @Model.WebLog.Name</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="~/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="~/">@Model.WebLog.Name</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<span class="navbar-text">@ViewBag.Title</span>
|
||||
@await Html.PartialAsync("_LogOnOffPartial")
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
@* Each.Messages
|
||||
@Current.ToDisplay
|
||||
@EndEach *@
|
||||
@RenderBody()
|
||||
</main>
|
||||
<footer>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-end"><img src="~/img/logo-light.png" alt="myWebLog"></div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
||||
crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
||||
@if (User is not null && (User.Identity?.IsAuthenticated ?? false))
|
||||
{
|
||||
<li class="nav-item"><a class="nav-link" asp-action="Index" asp-controller="Admin">@Resources.Dashboard</a></li>
|
||||
<li class="nav-item"><a class="nav-link" asp-action="LogOff" asp-controller="User">@Resources.LogOff</a></li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="nav-item"><a class="nav-link" asp-action="LogOn" asp-controller="User">@Resources.LogOn</a></li>
|
||||
}
|
||||
</ul>
|
||||
@@ -1,28 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
|
||||
namespace MyWebLog.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Expand the location token with the theme path
|
||||
/// </summary>
|
||||
public class ThemeViewLocationExpander : IViewLocationExpander
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
|
||||
IEnumerable<string> 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"]!);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PopulateValues(ViewLocationExpanderContext context)
|
||||
{
|
||||
_ = context ?? throw new ArgumentNullException(nameof(context));
|
||||
|
||||
context.Values["theme"] = WebLogCache.Get(context.ActionContext.HttpContext).ThemePath;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
@model LogOnModel
|
||||
@{
|
||||
Layout = "_AdminLayout";
|
||||
ViewBag.Title = @Resources.LogOn;
|
||||
}
|
||||
<h2 class="p-3 ">@Resources.LogOnTo @Model.WebLog.Name</h2>
|
||||
<article class="pb-3">
|
||||
<form asp-action="DoLogOn" asp-controller="User" method="post">
|
||||
<div class="container">
|
||||
<div class="row pb-3">
|
||||
<div class="col col-md-6 col-lg-4 offset-lg-2">
|
||||
<div class="form-floating">
|
||||
<input type="email" asp-for="EmailAddress" class="form-control" autofocus>
|
||||
<label asp-for="EmailAddress"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-lg-4">
|
||||
<div class="form-floating">
|
||||
<input type="password" asp-for="Password" class="form-control">
|
||||
<label asp-for="Password"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col text-center">
|
||||
<button type="submit" class="btn btn-primary">@Resources.LogOn</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MyWebLog.Features.Users;
|
||||
|
||||
/// <summary>
|
||||
/// The model to use to allow a user to log on
|
||||
/// </summary>
|
||||
public class LogOnModel : MyWebLogModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The user's e-mail address
|
||||
/// </summary>
|
||||
[Required(AllowEmptyStrings = false)]
|
||||
[EmailAddress]
|
||||
[Display(ResourceType = typeof(Resources), Name = "EmailAddress")]
|
||||
public string EmailAddress { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The user's password
|
||||
/// </summary>
|
||||
[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()) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public LogOnModel(WebLogDetails webLog) : base(webLog) { }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for the users feature
|
||||
/// </summary>
|
||||
[Route("/user")]
|
||||
public class UserController : MyWebLogController
|
||||
{
|
||||
/// <summary>
|
||||
/// Hash a password for a given user
|
||||
/// </summary>
|
||||
/// <param name="plainText">The plain-text password</param>
|
||||
/// <param name="email">The user's e-mail address</param>
|
||||
/// <param name="salt">The user-specific salt</param>
|
||||
/// <returns></returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UserController(WebLogDbContext db) : base(db) { }
|
||||
|
||||
[HttpGet("log-on")]
|
||||
public IActionResult LogOn() =>
|
||||
View(new LogOnModel(WebLog));
|
||||
|
||||
[HttpPost("log-on")]
|
||||
public async Task<IActionResult> 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<Claim> 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<IActionResult> LogOff()
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
// TODO: confirmation message
|
||||
|
||||
return LocalRedirect("~/");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@namespace MyWebLog.Features
|
||||
|
||||
@using MyWebLog
|
||||
@using MyWebLog.Properties
|
||||
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, MyWebLog
|
||||
@@ -1,3 +0,0 @@
|
||||
global using MyWebLog.Data;
|
||||
global using MyWebLog.Features.Shared;
|
||||
global using MyWebLog.Properties;
|
||||
59
src/MyWebLog/Handlers.fs
Normal file
59
src/MyWebLog/Handlers.fs
Normal file
@@ -0,0 +1,59 @@
|
||||
[<RequireQualifiedAccess>]
|
||||
module MyWebLog.Handlers
|
||||
|
||||
open Giraffe
|
||||
open MyWebLog
|
||||
open MyWebLog.ViewModels
|
||||
open System
|
||||
|
||||
[<AutoOpen>]
|
||||
module private Helpers =
|
||||
|
||||
open DotLiquid
|
||||
open System.Collections.Concurrent
|
||||
open System.IO
|
||||
|
||||
/// Cache for parsed templates
|
||||
let private themeViews = ConcurrentDictionary<string, Template> ()
|
||||
|
||||
/// Return a view for a theme
|
||||
let themedView<'T> (template : string) (model : obj) : HttpHandler = fun next ctx -> task {
|
||||
let webLog = WebLogCache.getByCtx ctx
|
||||
let templatePath = $"themes/{webLog.themePath}/{template}"
|
||||
match themeViews.ContainsKey templatePath with
|
||||
| true -> ()
|
||||
| false ->
|
||||
let! file = File.ReadAllTextAsync $"{templatePath}.liquid"
|
||||
themeViews[templatePath] <- Template.Parse file
|
||||
let view = themeViews[templatePath].Render (Hash.FromAnonymousObject model)
|
||||
return! htmlString view next ctx
|
||||
}
|
||||
|
||||
module User =
|
||||
|
||||
open System.Security.Cryptography
|
||||
open System.Text
|
||||
|
||||
/// 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))
|
||||
|
||||
|
||||
module CatchAll =
|
||||
|
||||
let catchAll : HttpHandler = fun next ctx -> task {
|
||||
let testPage = { Page.empty with text = "Howdy, folks!" }
|
||||
return! themedView "single-page" { page = testPage; webLog = WebLogCache.getByCtx ctx } next ctx
|
||||
}
|
||||
|
||||
open Giraffe.EndpointRouting
|
||||
|
||||
/// The endpoints defined in the above handlers
|
||||
let endpoints = [
|
||||
GET [
|
||||
route "" CatchAll.catchAll
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\img\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Markdig" Version="0.27.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyWebLog.DataCS\MyWebLog.DataCS.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
32
src/MyWebLog/MyWebLog.fsproj
Normal file
32
src/MyWebLog/MyWebLog.fsproj
Normal file
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json" CopyToOutputDirectory="Always" />
|
||||
<Compile Include="WebLogCache.fs" />
|
||||
<Compile Include="Handlers.fs" />
|
||||
<Compile Include="Program.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotLiquid" Version="2.2.610" />
|
||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyWebLog.Data\MyWebLog.Data.fsproj" />
|
||||
<ProjectReference Include="..\MyWebLog.Domain\MyWebLog.Domain.fsproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include=".\themes\**" CopyToOutputDirectory="Always" />
|
||||
<None Include=".\wwwroot\**" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup />
|
||||
|
||||
</Project>
|
||||
@@ -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<IHttpContextAccessor, HttpContextAccessor>();
|
||||
builder.Services.AddDbContext<WebLogDbContext>(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<WebLogMiddleware>();
|
||||
app.UseAuthentication();
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||
|
||||
app.Run();
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new database
|
||||
/// </summary>
|
||||
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<WebLogDbContext>()
|
||||
.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 = "<p>This is your default home page.</p>",
|
||||
Revisions = new[]
|
||||
{
|
||||
new PageRevision
|
||||
{
|
||||
Id = WebLogDbContext.NewId(),
|
||||
AsOf = DateTime.UtcNow,
|
||||
SourceType = RevisionSource.Html,
|
||||
Text = "<p>This is your default home page.</p>"
|
||||
}
|
||||
}
|
||||
};
|
||||
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]}");
|
||||
}
|
||||
150
src/MyWebLog/Program.fs
Normal file
150
src/MyWebLog/Program.fs
Normal file
@@ -0,0 +1,150 @@
|
||||
open Giraffe.EndpointRouting
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.Extensions.Configuration
|
||||
open Microsoft.Extensions.Hosting
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Microsoft.Extensions.Logging
|
||||
open MyWebLog
|
||||
open RethinkDb.Driver.FSharp
|
||||
open RethinkDb.Driver.Net
|
||||
open System
|
||||
|
||||
/// Middleware to derive the current web log
|
||||
type WebLogMiddleware (next : RequestDelegate) =
|
||||
|
||||
member this.InvokeAsync (ctx : HttpContext) = task {
|
||||
let host = ctx.Request.Host.ToUriComponent ()
|
||||
match WebLogCache.exists host with
|
||||
| true -> return! next.Invoke ctx
|
||||
| false ->
|
||||
let conn = ctx.RequestServices.GetRequiredService<IConnection> ()
|
||||
match! Data.WebLog.findByHost host conn with
|
||||
| Some webLog ->
|
||||
WebLogCache.set host webLog
|
||||
return! next.Invoke ctx
|
||||
| None -> ctx.Response.StatusCode <- 404
|
||||
}
|
||||
|
||||
|
||||
/// Initialize a new database
|
||||
let initDbValidated (args : string[]) (sp : IServiceProvider) = task {
|
||||
|
||||
let conn = sp.GetRequiredService<IConnection> ()
|
||||
|
||||
let timeZone =
|
||||
let local = TimeZoneInfo.Local.Id
|
||||
match TimeZoneInfo.Local.HasIanaId with
|
||||
| true -> local
|
||||
| false ->
|
||||
match TimeZoneInfo.TryConvertWindowsIdToIanaId local with
|
||||
| true, ianaId -> ianaId
|
||||
| false, _ -> raise <| TimeZoneNotFoundException $"Cannot find IANA timezone for {local}"
|
||||
|
||||
// Create the web log
|
||||
let webLogId = WebLogId.create ()
|
||||
let userId = WebLogUserId.create ()
|
||||
let homePageId = PageId.create ()
|
||||
|
||||
do! Data.WebLog.add
|
||||
{ WebLog.empty with
|
||||
id = webLogId
|
||||
name = args[2]
|
||||
urlBase = args[1]
|
||||
defaultPage = PageId.toString homePageId
|
||||
timeZone = timeZone
|
||||
} conn
|
||||
|
||||
// Create the admin user
|
||||
let salt = Guid.NewGuid ()
|
||||
|
||||
do! Data.WebLogUser.add
|
||||
{ WebLogUser.empty with
|
||||
id = userId
|
||||
webLogId = webLogId
|
||||
userName = args[3]
|
||||
firstName = "Admin"
|
||||
lastName = "User"
|
||||
preferredName = "Admin"
|
||||
passwordHash = Handlers.User.hashedPassword args[4] args[3] salt
|
||||
salt = salt
|
||||
authorizationLevel = Administrator
|
||||
} conn
|
||||
|
||||
// Create the default home page
|
||||
do! Data.Page.add
|
||||
{ Page.empty with
|
||||
id = homePageId
|
||||
webLogId = webLogId
|
||||
authorId = userId
|
||||
title = "Welcome to myWebLog!"
|
||||
permalink = Permalink "welcome-to-myweblog.html"
|
||||
publishedOn = DateTime.UtcNow
|
||||
updatedOn = DateTime.UtcNow
|
||||
text = "<p>This is your default home page.</p>"
|
||||
revisions = [
|
||||
{ asOf = DateTime.UtcNow
|
||||
sourceType = Html
|
||||
text = "<p>This is your default home page.</p>"
|
||||
}
|
||||
]
|
||||
} conn
|
||||
|
||||
Console.WriteLine($"Successfully initialized database for {args[2]} with URL base {args[1]}");
|
||||
}
|
||||
|
||||
/// Initialize a new database
|
||||
let initDb args sp = task {
|
||||
match args |> Array.length with
|
||||
| 5 -> return! initDbValidated args sp
|
||||
| _ ->
|
||||
Console.WriteLine "Usage: MyWebLog init [url] [name] [admin-email] [admin-pw]"
|
||||
return! System.Threading.Tasks.Task.CompletedTask
|
||||
}
|
||||
|
||||
|
||||
[<EntryPoint>]
|
||||
let main args =
|
||||
|
||||
let builder = WebApplication.CreateBuilder(args)
|
||||
let _ =
|
||||
builder.Services
|
||||
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(fun opts ->
|
||||
opts.ExpireTimeSpan <- TimeSpan.FromMinutes 20.
|
||||
opts.SlidingExpiration <- true
|
||||
opts.AccessDeniedPath <- "/forbidden")
|
||||
let _ = builder.Services.AddLogging ()
|
||||
let _ = builder.Services.AddAuthorization()
|
||||
|
||||
// Configure RethinkDB's connection
|
||||
JsonConverters.all () |> Seq.iter Converter.Serializer.Converters.Add
|
||||
let sp = builder.Services.BuildServiceProvider ()
|
||||
let config = sp.GetRequiredService<IConfiguration> ()
|
||||
let loggerFac = sp.GetRequiredService<ILoggerFactory> ()
|
||||
let rethinkCfg = DataConfig.FromConfiguration (config.GetSection "RethinkDB")
|
||||
let conn =
|
||||
task {
|
||||
let! conn = rethinkCfg.CreateConnectionAsync ()
|
||||
do! Data.Startup.ensureDb rethinkCfg (loggerFac.CreateLogger (nameof Data.Startup)) conn
|
||||
return conn
|
||||
} |> Async.AwaitTask |> Async.RunSynchronously
|
||||
let _ = builder.Services.AddSingleton<IConnection> conn
|
||||
|
||||
let app = builder.Build ()
|
||||
|
||||
match args |> Array.tryHead with
|
||||
| Some it when it = "init" -> initDb args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
||||
| _ ->
|
||||
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
||||
let _ = app.UseMiddleware<WebLogMiddleware> ()
|
||||
let _ = app.UseAuthentication ()
|
||||
let _ = app.UseStaticFiles ()
|
||||
let _ = app.UseRouting ()
|
||||
let _ = app.UseEndpoints (fun endpoints -> endpoints.MapGiraffeEndpoints Handlers.endpoints)
|
||||
|
||||
app.Run()
|
||||
|
||||
0 // Exit code
|
||||
|
||||
459
src/MyWebLog/Properties/Resources.Designer.cs
generated
459
src/MyWebLog/Properties/Resources.Designer.cs
generated
@@ -1,459 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 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.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace MyWebLog.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// 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() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Actions.
|
||||
/// </summary>
|
||||
public static string Actions {
|
||||
get {
|
||||
return ResourceManager.GetString("Actions", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Add a New Category.
|
||||
/// </summary>
|
||||
public static string AddANewCategory {
|
||||
get {
|
||||
return ResourceManager.GetString("AddANewCategory", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Add a New Page.
|
||||
/// </summary>
|
||||
public static string AddANewPage {
|
||||
get {
|
||||
return ResourceManager.GetString("AddANewPage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Admin.
|
||||
/// </summary>
|
||||
public static string Admin {
|
||||
get {
|
||||
return ResourceManager.GetString("Admin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to All.
|
||||
/// </summary>
|
||||
public static string All {
|
||||
get {
|
||||
return ResourceManager.GetString("All", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Categories.
|
||||
/// </summary>
|
||||
public static string Categories {
|
||||
get {
|
||||
return ResourceManager.GetString("Categories", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Create a New Page.
|
||||
/// </summary>
|
||||
public static string CreateANewPage {
|
||||
get {
|
||||
return ResourceManager.GetString("CreateANewPage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Dashboard.
|
||||
/// </summary>
|
||||
public static string Dashboard {
|
||||
get {
|
||||
return ResourceManager.GetString("Dashboard", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to MMMM d, yyyy.
|
||||
/// </summary>
|
||||
public static string DateFormatString {
|
||||
get {
|
||||
return ResourceManager.GetString("DateFormatString", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Default Page.
|
||||
/// </summary>
|
||||
public static string DefaultPage {
|
||||
get {
|
||||
return ResourceManager.GetString("DefaultPage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Drafts.
|
||||
/// </summary>
|
||||
public static string Drafts {
|
||||
get {
|
||||
return ResourceManager.GetString("Drafts", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit.
|
||||
/// </summary>
|
||||
public static string Edit {
|
||||
get {
|
||||
return ResourceManager.GetString("Edit", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit Page.
|
||||
/// </summary>
|
||||
public static string EditPage {
|
||||
get {
|
||||
return ResourceManager.GetString("EditPage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to E-mail Address.
|
||||
/// </summary>
|
||||
public static string EmailAddress {
|
||||
get {
|
||||
return ResourceManager.GetString("EmailAddress", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to First Page of Posts.
|
||||
/// </summary>
|
||||
public static string FirstPageOfPosts {
|
||||
get {
|
||||
return ResourceManager.GetString("FirstPageOfPosts", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to In List?.
|
||||
/// </summary>
|
||||
public static string InListQuestion {
|
||||
get {
|
||||
return ResourceManager.GetString("InListQuestion", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Last Updated.
|
||||
/// </summary>
|
||||
public static string LastUpdated {
|
||||
get {
|
||||
return ResourceManager.GetString("LastUpdated", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Log Off.
|
||||
/// </summary>
|
||||
public static string LogOff {
|
||||
get {
|
||||
return ResourceManager.GetString("LogOff", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Log On.
|
||||
/// </summary>
|
||||
public static string LogOn {
|
||||
get {
|
||||
return ResourceManager.GetString("LogOn", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Log On to.
|
||||
/// </summary>
|
||||
public static string LogOnTo {
|
||||
get {
|
||||
return ResourceManager.GetString("LogOnTo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Modify Settings.
|
||||
/// </summary>
|
||||
public static string ModifySettings {
|
||||
get {
|
||||
return ResourceManager.GetString("ModifySettings", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Name.
|
||||
/// </summary>
|
||||
public static string Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No.
|
||||
/// </summary>
|
||||
public static string No {
|
||||
get {
|
||||
return ResourceManager.GetString("No", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pages.
|
||||
/// </summary>
|
||||
public static string Pages {
|
||||
get {
|
||||
return ResourceManager.GetString("Pages", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Page Text.
|
||||
/// </summary>
|
||||
public static string PageText {
|
||||
get {
|
||||
return ResourceManager.GetString("PageText", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Password.
|
||||
/// </summary>
|
||||
public static string Password {
|
||||
get {
|
||||
return ResourceManager.GetString("Password", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Permalink.
|
||||
/// </summary>
|
||||
public static string Permalink {
|
||||
get {
|
||||
return ResourceManager.GetString("Permalink", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Posts.
|
||||
/// </summary>
|
||||
public static string Posts {
|
||||
get {
|
||||
return ResourceManager.GetString("Posts", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Posts per Page.
|
||||
/// </summary>
|
||||
public static string PostsPerPage {
|
||||
get {
|
||||
return ResourceManager.GetString("PostsPerPage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Published.
|
||||
/// </summary>
|
||||
public static string Published {
|
||||
get {
|
||||
return ResourceManager.GetString("Published", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save Changes.
|
||||
/// </summary>
|
||||
public static string SaveChanges {
|
||||
get {
|
||||
return ResourceManager.GetString("SaveChanges", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show in Page List.
|
||||
/// </summary>
|
||||
public static string ShowInPageList {
|
||||
get {
|
||||
return ResourceManager.GetString("ShowInPageList", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Shown in Page List.
|
||||
/// </summary>
|
||||
public static string ShownInPageList {
|
||||
get {
|
||||
return ResourceManager.GetString("ShownInPageList", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Subtitle.
|
||||
/// </summary>
|
||||
public static string Subtitle {
|
||||
get {
|
||||
return ResourceManager.GetString("Subtitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to There are {0} categories.
|
||||
/// </summary>
|
||||
public static string ThereAreXCategories {
|
||||
get {
|
||||
return ResourceManager.GetString("ThereAreXCategories", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to There are {0} pages.
|
||||
/// </summary>
|
||||
public static string ThereAreXPages {
|
||||
get {
|
||||
return ResourceManager.GetString("ThereAreXPages", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to There are {0} published posts and {1} drafts.
|
||||
/// </summary>
|
||||
public static string ThereAreXPublishedPostsAndYDrafts {
|
||||
get {
|
||||
return ResourceManager.GetString("ThereAreXPublishedPostsAndYDrafts", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Time Zone.
|
||||
/// </summary>
|
||||
public static string TimeZone {
|
||||
get {
|
||||
return ResourceManager.GetString("TimeZone", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Title.
|
||||
/// </summary>
|
||||
public static string Title {
|
||||
get {
|
||||
return ResourceManager.GetString("Title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Top Level.
|
||||
/// </summary>
|
||||
public static string TopLevel {
|
||||
get {
|
||||
return ResourceManager.GetString("TopLevel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to View All.
|
||||
/// </summary>
|
||||
public static string ViewAll {
|
||||
get {
|
||||
return ResourceManager.GetString("ViewAll", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Web Log Settings.
|
||||
/// </summary>
|
||||
public static string WebLogSettings {
|
||||
get {
|
||||
return ResourceManager.GetString("WebLogSettings", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Write a New Post.
|
||||
/// </summary>
|
||||
public static string WriteANewPost {
|
||||
get {
|
||||
return ResourceManager.GetString("WriteANewPost", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Yes.
|
||||
/// </summary>
|
||||
public static string Yes {
|
||||
get {
|
||||
return ResourceManager.GetString("Yes", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Actions" xml:space="preserve">
|
||||
<value>Actions</value>
|
||||
</data>
|
||||
<data name="AddANewCategory" xml:space="preserve">
|
||||
<value>Add a New Category</value>
|
||||
</data>
|
||||
<data name="AddANewPage" xml:space="preserve">
|
||||
<value>Add a New Page</value>
|
||||
</data>
|
||||
<data name="Admin" xml:space="preserve">
|
||||
<value>Admin</value>
|
||||
</data>
|
||||
<data name="All" xml:space="preserve">
|
||||
<value>All</value>
|
||||
</data>
|
||||
<data name="Categories" xml:space="preserve">
|
||||
<value>Categories</value>
|
||||
</data>
|
||||
<data name="CreateANewPage" xml:space="preserve">
|
||||
<value>Create a New Page</value>
|
||||
</data>
|
||||
<data name="Dashboard" xml:space="preserve">
|
||||
<value>Dashboard</value>
|
||||
</data>
|
||||
<data name="DateFormatString" xml:space="preserve">
|
||||
<value>MMMM d, yyyy</value>
|
||||
</data>
|
||||
<data name="DefaultPage" xml:space="preserve">
|
||||
<value>Default Page</value>
|
||||
</data>
|
||||
<data name="Drafts" xml:space="preserve">
|
||||
<value>Drafts</value>
|
||||
</data>
|
||||
<data name="Edit" xml:space="preserve">
|
||||
<value>Edit</value>
|
||||
</data>
|
||||
<data name="EditPage" xml:space="preserve">
|
||||
<value>Edit Page</value>
|
||||
</data>
|
||||
<data name="EmailAddress" xml:space="preserve">
|
||||
<value>E-mail Address</value>
|
||||
</data>
|
||||
<data name="FirstPageOfPosts" xml:space="preserve">
|
||||
<value>First Page of Posts</value>
|
||||
</data>
|
||||
<data name="InListQuestion" xml:space="preserve">
|
||||
<value>In List?</value>
|
||||
</data>
|
||||
<data name="LastUpdated" xml:space="preserve">
|
||||
<value>Last Updated</value>
|
||||
</data>
|
||||
<data name="LogOff" xml:space="preserve">
|
||||
<value>Log Off</value>
|
||||
</data>
|
||||
<data name="LogOn" xml:space="preserve">
|
||||
<value>Log On</value>
|
||||
</data>
|
||||
<data name="LogOnTo" xml:space="preserve">
|
||||
<value>Log On to</value>
|
||||
</data>
|
||||
<data name="ModifySettings" xml:space="preserve">
|
||||
<value>Modify Settings</value>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
<data name="No" xml:space="preserve">
|
||||
<value>No</value>
|
||||
</data>
|
||||
<data name="Pages" xml:space="preserve">
|
||||
<value>Pages</value>
|
||||
</data>
|
||||
<data name="PageText" xml:space="preserve">
|
||||
<value>Page Text</value>
|
||||
</data>
|
||||
<data name="Password" xml:space="preserve">
|
||||
<value>Password</value>
|
||||
</data>
|
||||
<data name="Permalink" xml:space="preserve">
|
||||
<value>Permalink</value>
|
||||
</data>
|
||||
<data name="Posts" xml:space="preserve">
|
||||
<value>Posts</value>
|
||||
</data>
|
||||
<data name="PostsPerPage" xml:space="preserve">
|
||||
<value>Posts per Page</value>
|
||||
</data>
|
||||
<data name="Published" xml:space="preserve">
|
||||
<value>Published</value>
|
||||
</data>
|
||||
<data name="SaveChanges" xml:space="preserve">
|
||||
<value>Save Changes</value>
|
||||
</data>
|
||||
<data name="ShowInPageList" xml:space="preserve">
|
||||
<value>Show in Page List</value>
|
||||
</data>
|
||||
<data name="ShownInPageList" xml:space="preserve">
|
||||
<value>Shown in Page List</value>
|
||||
</data>
|
||||
<data name="Subtitle" xml:space="preserve">
|
||||
<value>Subtitle</value>
|
||||
</data>
|
||||
<data name="ThereAreXCategories" xml:space="preserve">
|
||||
<value>There are {0} categories</value>
|
||||
</data>
|
||||
<data name="ThereAreXPages" xml:space="preserve">
|
||||
<value>There are {0} pages</value>
|
||||
</data>
|
||||
<data name="ThereAreXPublishedPostsAndYDrafts" xml:space="preserve">
|
||||
<value>There are {0} published posts and {1} drafts</value>
|
||||
</data>
|
||||
<data name="TimeZone" xml:space="preserve">
|
||||
<value>Time Zone</value>
|
||||
</data>
|
||||
<data name="Title" xml:space="preserve">
|
||||
<value>Title</value>
|
||||
</data>
|
||||
<data name="TopLevel" xml:space="preserve">
|
||||
<value>Top Level</value>
|
||||
</data>
|
||||
<data name="ViewAll" xml:space="preserve">
|
||||
<value>View All</value>
|
||||
</data>
|
||||
<data name="WebLogSettings" xml:space="preserve">
|
||||
<value>Web Log Settings</value>
|
||||
</data>
|
||||
<data name="WriteANewPost" xml:space="preserve">
|
||||
<value>Write a New Post</value>
|
||||
</data>
|
||||
<data name="Yes" xml:space="preserve">
|
||||
<value>Yes</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
@model MyWebLogModel
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="generator" content="myWebLog 2">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link asp-theme="@Model.WebLog.ThemePath" />
|
||||
@await RenderSectionAsync("Style", false)
|
||||
<title>@ViewBag.Title « @Model.WebLog.Name</title>
|
||||
</head>
|
||||
<body>
|
||||
@if (IsSectionDefined("Header"))
|
||||
{
|
||||
@await RenderSectionAsync("Header")
|
||||
}
|
||||
else
|
||||
{
|
||||
@await Html.PartialAsync("_DefaultHeader", Model)
|
||||
}
|
||||
<main>
|
||||
@RenderBody()
|
||||
</main>
|
||||
@if (IsSectionDefined("Footer"))
|
||||
{
|
||||
@await RenderSectionAsync("Footer")
|
||||
} else
|
||||
{
|
||||
@await Html.PartialAsync("_DefaultFooter")
|
||||
}
|
||||
@await RenderSectionAsync("Script", false)
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +0,0 @@
|
||||
@using MyWebLog.Features.Pages
|
||||
@model SinglePageModel
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
ViewBag.Title = Model.Page.Title;
|
||||
}
|
||||
<h2>@Model.Page.Title</h2>
|
||||
<article>
|
||||
@Html.Raw(Model.Page.Text)
|
||||
</article>
|
||||
@@ -1,3 +0,0 @@
|
||||
@namespace MyWebLog.Themes
|
||||
|
||||
@addTagHelper *, MyWebLog
|
||||
24
src/MyWebLog/WebLogCache.fs
Normal file
24
src/MyWebLog/WebLogCache.fs
Normal file
@@ -0,0 +1,24 @@
|
||||
/// <summary>
|
||||
/// In-memory cache of web log details
|
||||
/// </summary>
|
||||
/// <remarks>This is filled by the middleware via the first request for each host, and can be updated via the web log
|
||||
/// settings update page</remarks>
|
||||
module MyWebLog.WebLogCache
|
||||
|
||||
open Microsoft.AspNetCore.Http
|
||||
open System.Collections.Concurrent
|
||||
|
||||
/// The cache of web log details
|
||||
let private _cache = ConcurrentDictionary<string, WebLog> ()
|
||||
|
||||
/// 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
|
||||
@@ -1,91 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MyWebLog;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory cache of web log details
|
||||
/// </summary>
|
||||
/// <remarks>This is filled by the middleware via the first request for each host, and can be updated via the web log
|
||||
/// settings update page</remarks>
|
||||
public static class WebLogCache
|
||||
{
|
||||
/// <summary>
|
||||
/// The cache of web log details
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, WebLogDetails> _cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Transform a hostname to a database name
|
||||
/// </summary>
|
||||
/// <param name="ctx">The current HTTP context</param>
|
||||
/// <returns>The hostname, with an underscore replacing a colon</returns>
|
||||
public static string HostToDb(HttpContext ctx) => ctx.Request.Host.ToUriComponent().Replace(':', '_');
|
||||
|
||||
/// <summary>
|
||||
/// Does a host exist in the cache?
|
||||
/// </summary>
|
||||
/// <param name="host">The host in question</param>
|
||||
/// <returns>True if it exists, false if not</returns>
|
||||
public static bool Exists(string host) => _cache.ContainsKey(host);
|
||||
|
||||
/// <summary>
|
||||
/// Get the details for a web log via its host
|
||||
/// </summary>
|
||||
/// <param name="host">The host which should be retrieved</param>
|
||||
/// <returns>The web log details</returns>
|
||||
public static WebLogDetails Get(string host) => _cache[host];
|
||||
|
||||
/// <summary>
|
||||
/// Get the details for a web log via its host
|
||||
/// </summary>
|
||||
/// <param name="ctx">The HTTP context for the request</param>
|
||||
/// <returns>The web log details</returns>
|
||||
public static WebLogDetails Get(HttpContext ctx) => _cache[HostToDb(ctx)];
|
||||
|
||||
/// <summary>
|
||||
/// Set the details for a particular host
|
||||
/// </summary>
|
||||
/// <param name="host">The host for which details should be set</param>
|
||||
/// <param name="details">The details to be set</param>
|
||||
public static void Set(string host, WebLogDetails details) => _cache[host] = details;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Middleware to derive the current web log
|
||||
/// </summary>
|
||||
public class WebLogMiddleware
|
||||
{
|
||||
/// <summary>
|
||||
/// The next action in the pipeline
|
||||
/// </summary>
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="next">The next action in the pipeline</param>
|
||||
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<WebLogDbContext>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
{
|
||||
"RethinkDB": {
|
||||
"hostname": "data02.bitbadger.solutions",
|
||||
"database": "myWebLog-dev"
|
||||
}
|
||||
}
|
||||
|
||||
11
src/MyWebLog/themes/default/_html-head.liquid
Normal file
11
src/MyWebLog/themes/default/_html-head.liquid
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="generator" content="myWebLog 2">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link asp-theme="@Model.WebLog.ThemePath" />
|
||||
<title>{{ title | escape }} « {{ web_log_name | escape }}</title>
|
||||
</head>
|
||||
@@ -1,6 +1,6 @@
|
||||
<footer>
|
||||
<hr>
|
||||
<div class="container-fluid text-end">
|
||||
<img src="~/img/logo-dark.png" alt="myWebLog">
|
||||
<img src="/img/logo-dark.png" alt="myWebLog">
|
||||
</div>
|
||||
</footer>
|
||||
@@ -1,17 +1,15 @@
|
||||
@model MyWebLogModel
|
||||
<header>
|
||||
<header>
|
||||
<nav class="navbar navbar-light bg-light navbar-expand-md justify-content-start px-2">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="~/">@Model.WebLog.Name</a>
|
||||
<a class="navbar-brand" href="~/">{{ web_log.name }}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
@if (Model.WebLog.Subtitle is not null)
|
||||
{
|
||||
<span class="navbar-text">@Model.WebLog.Subtitle</span>
|
||||
}
|
||||
{% if web_log.subtitle -%}
|
||||
<span class="navbar-text">{{ web_log.subtitle | escape }}</span>
|
||||
{%- endif %}
|
||||
@* TODO: list pages for current web log *@
|
||||
@await Html.PartialAsync("_LogOnOffPartial")
|
||||
</div>
|
||||
14
src/MyWebLog/themes/default/single-page.liquid
Normal file
14
src/MyWebLog/themes/default/single-page.liquid
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{ render "_html-head", title: title, web_log_name: web_log.name }}
|
||||
<body>
|
||||
{{ render "_page-head", web_log: web_log }}
|
||||
<main>
|
||||
<h2>{{ page.title }}</h2>
|
||||
<article>
|
||||
{{ page.text }}
|
||||
</article>
|
||||
</main>
|
||||
{{ render "_page-foot" }}
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB |
Reference in New Issue
Block a user