Init data, single home page, multi-tenant
This commit is contained in:
parent
8bc2cf0aa5
commit
df3467030a
14
src/MyWebLog.Data/Extensions/PageExtensions.cs
Normal file
14
src/MyWebLog.Data/Extensions/PageExtensions.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace MyWebLog.Data;
|
||||||
|
|
||||||
|
public static class PageExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve a page by its ID (non-tracked)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the page to retrieve</param>
|
||||||
|
/// <returns>The requested page (or null if it is not found)</returns>
|
||||||
|
public static async Task<Page?> FindById(this DbSet<Page> db, string id) =>
|
||||||
|
await db.FirstOrDefaultAsync(p => p.Id == id).ConfigureAwait(false);
|
||||||
|
}
|
17
src/MyWebLog.Data/Extensions/PostExtensions.cs
Normal file
17
src/MyWebLog.Data/Extensions/PostExtensions.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace MyWebLog.Data;
|
||||||
|
|
||||||
|
public static class PostExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve a page of published posts (non-tracked)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pageNbr">The page number to retrieve</param>
|
||||||
|
/// <param name="postsPerPage">The number of posts per page</param>
|
||||||
|
/// <returns>A list of posts representing the posts for the given page</returns>
|
||||||
|
public static async Task<List<Post>> FindPageOfPublishedPosts(this DbSet<Post> db, int pageNbr, int postsPerPage) =>
|
||||||
|
await db.Where(p => p.Status == PostStatus.Published)
|
||||||
|
.Skip((pageNbr - 1) * postsPerPage).Take(postsPerPage)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
14
src/MyWebLog.Data/Extensions/WebLogDetailsExtensions.cs
Normal file
14
src/MyWebLog.Data/Extensions/WebLogDetailsExtensions.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace MyWebLog.Data;
|
||||||
|
|
||||||
|
public static class WebLogDetailsExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Find the details of a web log by its host
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="host">The host</param>
|
||||||
|
/// <returns>The web log (or null if not found)</returns>
|
||||||
|
public static async Task<WebLogDetails?> FindByHost(this DbSet<WebLogDetails> db, string host) =>
|
||||||
|
await db.FirstOrDefaultAsync(wld => wld.UrlBase == host).ConfigureAwait(false);
|
||||||
|
}
|
518
src/MyWebLog.Data/Migrations/20220227160816_Initial.Designer.cs
generated
Normal file
518
src/MyWebLog.Data/Migrations/20220227160816_Initial.Designer.cs
generated
Normal file
|
@ -0,0 +1,518 @@
|
||||||
|
// <auto-generated />
|
||||||
|
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("20220227160816_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<string>("CategoriesId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PostsId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("CategoriesId", "PostsId");
|
||||||
|
|
||||||
|
b.HasIndex("PostsId");
|
||||||
|
|
||||||
|
b.ToTable("CategoryPost");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ParentId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.HasIndex("Slug");
|
||||||
|
|
||||||
|
b.ToTable("Category");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Comment", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("InReplyToId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PostId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PostedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InReplyToId");
|
||||||
|
|
||||||
|
b.HasIndex("PostId");
|
||||||
|
|
||||||
|
b.ToTable("Comment");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Page", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Permalink")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PublishedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowInPageList")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuthorId");
|
||||||
|
|
||||||
|
b.HasIndex("Permalink");
|
||||||
|
|
||||||
|
b.ToTable("Page");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.PagePermalink", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PageId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PageId");
|
||||||
|
|
||||||
|
b.ToTable("PagePermalink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.PageRevision", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AsOf")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PageId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SourceType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PageId");
|
||||||
|
|
||||||
|
b.ToTable("PageRevision");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Post", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Permalink")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PublishedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuthorId");
|
||||||
|
|
||||||
|
b.HasIndex("Permalink");
|
||||||
|
|
||||||
|
b.ToTable("Post");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.PostPermalink", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PostId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PostId");
|
||||||
|
|
||||||
|
b.ToTable("PostPermalink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.PostRevision", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AsOf")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PostId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SourceType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PostId");
|
||||||
|
|
||||||
|
b.ToTable("PostRevision");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("Tag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.WebLogDetails", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte>("PostsPerPage")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Subtitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ThemePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZone")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UrlBase")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("WebLogDetails");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.WebLogUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("AuthorizationLevel")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PreferredName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("Salt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("WebLogUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PostTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("PostsId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
398
src/MyWebLog.Data/Migrations/20220227160816_Initial.cs
Normal file
398
src/MyWebLog.Data/Migrations/20220227160816_Initial.cs
Normal file
|
@ -0,0 +1,398 @@
|
||||||
|
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<string>(type: "TEXT", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
ParentId = table.Column<string>(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<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Tag", x => x.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "WebLogDetails",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Subtitle = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
DefaultPage = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
PostsPerPage = table.Column<byte>(type: "INTEGER", nullable: false),
|
||||||
|
ThemePath = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
UrlBase = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
TimeZone = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_WebLogDetails", x => x.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "WebLogUser",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
UserName = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
FirstName = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
LastName = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
PreferredName = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
PasswordHash = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Salt = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
Url = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
AuthorizationLevel = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_WebLogUser", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Page",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
AuthorId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Permalink = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
PublishedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
ShowInPageList = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Text = table.Column<string>(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<string>(type: "TEXT", nullable: false),
|
||||||
|
AuthorId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Permalink = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
PublishedOn = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Text = table.Column<string>(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<string>(type: "TEXT", nullable: false),
|
||||||
|
PageId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Url = table.Column<string>(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<string>(type: "TEXT", nullable: false),
|
||||||
|
PageId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
AsOf = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
SourceType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Text = table.Column<string>(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<string>(type: "TEXT", nullable: false),
|
||||||
|
PostsId = table.Column<string>(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<string>(type: "TEXT", nullable: false),
|
||||||
|
PostId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
InReplyToId = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Email = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Url = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
PostedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Text = table.Column<string>(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<string>(type: "TEXT", nullable: false),
|
||||||
|
PostId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Url = table.Column<string>(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<string>(type: "TEXT", nullable: false),
|
||||||
|
PostId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
AsOf = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
SourceType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Text = table.Column<string>(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<string>(type: "TEXT", nullable: false),
|
||||||
|
TagsName = table.Column<string>(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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
516
src/MyWebLog.Data/Migrations/WebLogDbContextModelSnapshot.cs
Normal file
516
src/MyWebLog.Data/Migrations/WebLogDbContextModelSnapshot.cs
Normal file
|
@ -0,0 +1,516 @@
|
||||||
|
// <auto-generated />
|
||||||
|
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<string>("CategoriesId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PostsId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("CategoriesId", "PostsId");
|
||||||
|
|
||||||
|
b.HasIndex("PostsId");
|
||||||
|
|
||||||
|
b.ToTable("CategoryPost");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ParentId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.HasIndex("Slug");
|
||||||
|
|
||||||
|
b.ToTable("Category");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Comment", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("InReplyToId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PostId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PostedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InReplyToId");
|
||||||
|
|
||||||
|
b.HasIndex("PostId");
|
||||||
|
|
||||||
|
b.ToTable("Comment");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Page", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Permalink")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PublishedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowInPageList")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuthorId");
|
||||||
|
|
||||||
|
b.HasIndex("Permalink");
|
||||||
|
|
||||||
|
b.ToTable("Page");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.PagePermalink", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PageId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PageId");
|
||||||
|
|
||||||
|
b.ToTable("PagePermalink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.PageRevision", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AsOf")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PageId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SourceType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PageId");
|
||||||
|
|
||||||
|
b.ToTable("PageRevision");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Post", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Permalink")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PublishedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedOn")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuthorId");
|
||||||
|
|
||||||
|
b.HasIndex("Permalink");
|
||||||
|
|
||||||
|
b.ToTable("Post");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.PostPermalink", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PostId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PostId");
|
||||||
|
|
||||||
|
b.ToTable("PostPermalink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.PostRevision", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AsOf")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PostId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SourceType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PostId");
|
||||||
|
|
||||||
|
b.ToTable("PostRevision");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("Tag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.WebLogDetails", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte>("PostsPerPage")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Subtitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ThemePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("TimeZone")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UrlBase")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("WebLogDetails");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyWebLog.Data.WebLogUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("AuthorizationLevel")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PreferredName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("Salt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("WebLogUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PostTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("PostsId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,4 +6,15 @@
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateRuntimeConfigurationFiles>True</GenerateRuntimeConfigurationFiles>
|
||||||
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -7,6 +7,14 @@ namespace MyWebLog.Data;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WebLogDbContext : DbContext
|
public sealed class WebLogDbContext : DbContext
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new ID (short GUID)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A new short GUID</returns>
|
||||||
|
/// <remarks>https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID</remarks>
|
||||||
|
public static string NewId() =>
|
||||||
|
Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('/', '_').Replace('+', '-')[..22];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The categories for the web log
|
/// The categories for the web log
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -53,6 +61,10 @@ public sealed class WebLogDbContext : DbContext
|
||||||
{
|
{
|
||||||
base.OnModelCreating(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
|
// Tag and WebLogDetails use Name as its ID
|
||||||
modelBuilder.Entity<Tag>().HasKey(t => t.Name);
|
modelBuilder.Entity<Tag>().HasKey(t => t.Name);
|
||||||
modelBuilder.Entity<WebLogDetails>().HasKey(wld => wld.Name);
|
modelBuilder.Entity<WebLogDetails>().HasKey(wld => wld.Name);
|
||||||
|
|
|
@ -20,10 +20,15 @@ public class WebLogDetails
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DefaultPage { get; set; } = "";
|
public string DefaultPage { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of posts to display on pages of posts
|
||||||
|
/// </summary>
|
||||||
|
public byte PostsPerPage { get; set; } = 10;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The path of the theme (within /views/themes)
|
/// The path of the theme (within /views/themes)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ThemePath { get; set; } = "";
|
public string ThemePath { get; set; } = "Default";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The URL base
|
/// The URL base
|
||||||
|
|
|
@ -45,6 +45,11 @@ public class WebLogUser
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Url { get; set; } = null;
|
public string? Url { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's authorization level
|
||||||
|
/// </summary>
|
||||||
|
public AuthorizationLevel AuthorizationLevel { get; set; } = AuthorizationLevel.User;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pages written by this author
|
/// Pages written by this author
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.1.32210.238
|
VisualStudioVersion = 17.1.32210.238
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog", "MyWebLog\MyWebLog.fsproj", "{2E5E2346-25FE-4CBD-89AA-6148A33DE09C}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.Data", "MyWebLog.Data\MyWebLog.Data.csproj", "{0177C744-F913-4352-A0EC-478B4B0388C3}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyWebLog.Data", "MyWebLog.Data\MyWebLog.Data.csproj", "{0177C744-F913-4352-A0EC-478B4B0388C3}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyWebLog", "MyWebLog\MyWebLog.csproj", "{3139DA09-C999-465A-BC98-02FEC3BD7E88}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
@ -13,14 +13,14 @@ Global
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{2E5E2346-25FE-4CBD-89AA-6148A33DE09C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{2E5E2346-25FE-4CBD-89AA-6148A33DE09C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{2E5E2346-25FE-4CBD-89AA-6148A33DE09C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{2E5E2346-25FE-4CBD-89AA-6148A33DE09C}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{0177C744-F913-4352-A0EC-478B4B0388C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{0177C744-F913-4352-A0EC-478B4B0388C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{0177C744-F913-4352-A0EC-478B4B0388C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0177C744-F913-4352-A0EC-478B4B0388C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{0177C744-F913-4352-A0EC-478B4B0388C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0177C744-F913-4352-A0EC-478B4B0388C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{0177C744-F913-4352-A0EC-478B4B0388C3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0177C744-F913-4352-A0EC-478B4B0388C3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3139DA09-C999-465A-BC98-02FEC3BD7E88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3139DA09-C999-465A-BC98-02FEC3BD7E88}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3139DA09-C999-465A-BC98-02FEC3BD7E88}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3139DA09-C999-465A-BC98-02FEC3BD7E88}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
1
src/MyWebLog/Db/.gitignore
vendored
Normal file
1
src/MyWebLog/Db/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*.db*
|
|
@ -1,489 +0,0 @@
|
||||||
namespace MyWebLog.Domain
|
|
||||||
|
|
||||||
// -- Supporting Types --
|
|
||||||
|
|
||||||
/// Types of markup text supported
|
|
||||||
type MarkupText =
|
|
||||||
/// Text in Markdown format
|
|
||||||
| Markdown of string
|
|
||||||
/// Text in HTML format
|
|
||||||
| Html of string
|
|
||||||
|
|
||||||
/// Functions to support maniuplating markup text
|
|
||||||
module MarkupText =
|
|
||||||
/// Get the string representation of this markup text
|
|
||||||
let toString it =
|
|
||||||
match it with
|
|
||||||
| Markdown x -> "Markdown", x
|
|
||||||
| Html x -> "HTML", x
|
|
||||||
||> sprintf "%s: %s"
|
|
||||||
/// Get the HTML value of the text
|
|
||||||
let toHtml = function
|
|
||||||
| Markdown it -> sprintf "TODO: convert to HTML - %s" it
|
|
||||||
| Html it -> it
|
|
||||||
/// Parse a string representation to markup text
|
|
||||||
let ofString (it : string) =
|
|
||||||
match true with
|
|
||||||
| _ when it.StartsWith "Markdown: " -> it.Substring 10 |> Markdown
|
|
||||||
| _ when it.StartsWith "HTML: " -> it.Substring 6 |> Html
|
|
||||||
| _ -> sprintf "Cannot determine text type - %s" it |> invalidOp
|
|
||||||
|
|
||||||
|
|
||||||
/// Authorization levels
|
|
||||||
type AuthorizationLevel =
|
|
||||||
/// Authorization to administer a weblog
|
|
||||||
| Administrator
|
|
||||||
/// Authorization to comment on a weblog
|
|
||||||
| User
|
|
||||||
|
|
||||||
/// Functions to support authorization levels
|
|
||||||
module AuthorizationLevel =
|
|
||||||
/// Get the string reprsentation of an authorization level
|
|
||||||
let toString = function Administrator -> "Administrator" | User -> "User"
|
|
||||||
/// Create an authorization level from a string
|
|
||||||
let ofString it =
|
|
||||||
match it with
|
|
||||||
| "Administrator" -> Administrator
|
|
||||||
| "User" -> User
|
|
||||||
| _ -> sprintf "%s is not an authorization level" it |> invalidOp
|
|
||||||
|
|
||||||
|
|
||||||
/// Post statuses
|
|
||||||
type PostStatus =
|
|
||||||
/// Post has not been released for public consumption
|
|
||||||
| Draft
|
|
||||||
/// Post is released
|
|
||||||
| Published
|
|
||||||
|
|
||||||
/// Functions to support post statuses
|
|
||||||
module PostStatus =
|
|
||||||
/// Get the string representation of a post status
|
|
||||||
let toString = function Draft -> "Draft" | Published -> "Published"
|
|
||||||
/// Create a post status from a string
|
|
||||||
let ofString it =
|
|
||||||
match it with
|
|
||||||
| "Draft" -> Draft
|
|
||||||
| "Published" -> Published
|
|
||||||
| _ -> sprintf "%s is not a post status" it |> invalidOp
|
|
||||||
|
|
||||||
|
|
||||||
/// Comment statuses
|
|
||||||
type CommentStatus =
|
|
||||||
/// Comment is approved
|
|
||||||
| Approved
|
|
||||||
/// Comment has yet to be approved
|
|
||||||
| Pending
|
|
||||||
/// Comment was flagged as spam
|
|
||||||
| Spam
|
|
||||||
|
|
||||||
/// Functions to support comment statuses
|
|
||||||
module CommentStatus =
|
|
||||||
/// Get the string representation of a comment status
|
|
||||||
let toString = function Approved -> "Approved" | Pending -> "Pending" | Spam -> "Spam"
|
|
||||||
/// Create a comment status from a string
|
|
||||||
let ofString it =
|
|
||||||
match it with
|
|
||||||
| "Approved" -> Approved
|
|
||||||
| "Pending" -> Pending
|
|
||||||
| "Spam" -> Spam
|
|
||||||
| _ -> sprintf "%s is not a comment status" it |> invalidOp
|
|
||||||
|
|
||||||
|
|
||||||
/// Seconds since the Unix epoch
|
|
||||||
type UnixSeconds = UnixSeconds of int64
|
|
||||||
|
|
||||||
/// Functions to support Unix seconds
|
|
||||||
module UnixSeconds =
|
|
||||||
/// Get the long (int64) representation of Unix seconds
|
|
||||||
let toLong = function UnixSeconds it -> it
|
|
||||||
/// Zero seconds past the epoch
|
|
||||||
let none = UnixSeconds 0L
|
|
||||||
|
|
||||||
|
|
||||||
// -- IDs --
|
|
||||||
|
|
||||||
open System
|
|
||||||
|
|
||||||
// See https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID for info on "short GUIDs"
|
|
||||||
|
|
||||||
/// A short GUID
|
|
||||||
type ShortGuid = ShortGuid of Guid
|
|
||||||
|
|
||||||
/// Functions to support short GUIDs
|
|
||||||
module ShortGuid =
|
|
||||||
/// Encode a GUID into a short GUID
|
|
||||||
let toString = function
|
|
||||||
| ShortGuid guid ->
|
|
||||||
Convert.ToBase64String(guid.ToByteArray ())
|
|
||||||
.Replace("/", "_")
|
|
||||||
.Replace("+", "-")
|
|
||||||
.Substring (0, 22)
|
|
||||||
/// Decode a short GUID into a GUID
|
|
||||||
let ofString (it : string) =
|
|
||||||
it.Replace("_", "/").Replace ("-", "+")
|
|
||||||
|> (sprintf "%s==" >> Convert.FromBase64String >> Guid >> ShortGuid)
|
|
||||||
/// Create a new short GUID
|
|
||||||
let create () = (Guid.NewGuid >> ShortGuid) ()
|
|
||||||
/// The empty short GUID
|
|
||||||
let empty = ShortGuid Guid.Empty
|
|
||||||
|
|
||||||
|
|
||||||
/// The ID of a category
|
|
||||||
type CategoryId = CategoryId of ShortGuid
|
|
||||||
|
|
||||||
/// Functions to support category IDs
|
|
||||||
module CategoryId =
|
|
||||||
/// Get the string representation of a page ID
|
|
||||||
let toString = function CategoryId it -> ShortGuid.toString it
|
|
||||||
/// Create a category ID from its string representation
|
|
||||||
let ofString = ShortGuid.ofString >> CategoryId
|
|
||||||
/// An empty category ID
|
|
||||||
let empty = CategoryId ShortGuid.empty
|
|
||||||
|
|
||||||
|
|
||||||
/// The ID of a comment
|
|
||||||
type CommentId = CommentId of ShortGuid
|
|
||||||
|
|
||||||
/// Functions to support comment IDs
|
|
||||||
module CommentId =
|
|
||||||
/// Get the string representation of a comment ID
|
|
||||||
let toString = function CommentId it -> ShortGuid.toString it
|
|
||||||
/// Create a comment ID from its string representation
|
|
||||||
let ofString = ShortGuid.ofString >> CommentId
|
|
||||||
/// An empty comment ID
|
|
||||||
let empty = CommentId ShortGuid.empty
|
|
||||||
|
|
||||||
|
|
||||||
/// The ID of a page
|
|
||||||
type PageId = PageId of ShortGuid
|
|
||||||
|
|
||||||
/// Functions to support page IDs
|
|
||||||
module PageId =
|
|
||||||
/// Get the string representation of a page ID
|
|
||||||
let toString = function PageId it -> ShortGuid.toString it
|
|
||||||
/// Create a page ID from its string representation
|
|
||||||
let ofString = ShortGuid.ofString >> PageId
|
|
||||||
/// An empty page ID
|
|
||||||
let empty = PageId ShortGuid.empty
|
|
||||||
|
|
||||||
|
|
||||||
/// The ID of a post
|
|
||||||
type PostId = PostId of ShortGuid
|
|
||||||
|
|
||||||
/// Functions to support post IDs
|
|
||||||
module PostId =
|
|
||||||
/// Get the string representation of a post ID
|
|
||||||
let toString = function PostId it -> ShortGuid.toString it
|
|
||||||
/// Create a post ID from its string representation
|
|
||||||
let ofString = ShortGuid.ofString >> PostId
|
|
||||||
/// An empty post ID
|
|
||||||
let empty = PostId ShortGuid.empty
|
|
||||||
|
|
||||||
|
|
||||||
/// The ID of a user
|
|
||||||
type UserId = UserId of ShortGuid
|
|
||||||
|
|
||||||
/// Functions to support user IDs
|
|
||||||
module UserId =
|
|
||||||
/// Get the string representation of a user ID
|
|
||||||
let toString = function UserId it -> ShortGuid.toString it
|
|
||||||
/// Create a user ID from its string representation
|
|
||||||
let ofString = ShortGuid.ofString >> UserId
|
|
||||||
/// An empty user ID
|
|
||||||
let empty = UserId ShortGuid.empty
|
|
||||||
|
|
||||||
|
|
||||||
/// The ID of a web log
|
|
||||||
type WebLogId = WebLogId of ShortGuid
|
|
||||||
|
|
||||||
/// Functions to support web log IDs
|
|
||||||
module WebLogId =
|
|
||||||
/// Get the string representation of a web log ID
|
|
||||||
let toString = function WebLogId it -> ShortGuid.toString it
|
|
||||||
/// Create a web log ID from its string representation
|
|
||||||
let ofString = ShortGuid.ofString >> WebLogId
|
|
||||||
/// An empty web log ID
|
|
||||||
let empty = WebLogId ShortGuid.empty
|
|
||||||
|
|
||||||
|
|
||||||
// -- Domain Entities --
|
|
||||||
// fsharplint:disable RecordFieldNames
|
|
||||||
|
|
||||||
/// A revision of a post or page
|
|
||||||
type Revision = {
|
|
||||||
/// The instant which this revision was saved
|
|
||||||
asOf : UnixSeconds
|
|
||||||
/// The text
|
|
||||||
text : MarkupText
|
|
||||||
}
|
|
||||||
with
|
|
||||||
/// An empty revision
|
|
||||||
static member empty =
|
|
||||||
{ asOf = UnixSeconds.none
|
|
||||||
text = Markdown ""
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// A page with static content
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type Page = {
|
|
||||||
/// The Id
|
|
||||||
id : PageId
|
|
||||||
/// The Id of the web log to which this page belongs
|
|
||||||
webLogId : WebLogId
|
|
||||||
/// The Id of the author of this page
|
|
||||||
authorId : UserId
|
|
||||||
/// The title of the page
|
|
||||||
title : string
|
|
||||||
/// The link at which this page is displayed
|
|
||||||
permalink : string
|
|
||||||
/// The instant this page was published
|
|
||||||
publishedOn : UnixSeconds
|
|
||||||
/// The instant this page was last updated
|
|
||||||
updatedOn : UnixSeconds
|
|
||||||
/// Whether this page shows as part of the web log's navigation
|
|
||||||
showInPageList : bool
|
|
||||||
/// The current text of the page
|
|
||||||
text : MarkupText
|
|
||||||
/// Revisions of this page
|
|
||||||
revisions : Revision list
|
|
||||||
}
|
|
||||||
with
|
|
||||||
static member empty =
|
|
||||||
{ id = PageId.empty
|
|
||||||
webLogId = WebLogId.empty
|
|
||||||
authorId = UserId.empty
|
|
||||||
title = ""
|
|
||||||
permalink = ""
|
|
||||||
publishedOn = UnixSeconds.none
|
|
||||||
updatedOn = UnixSeconds.none
|
|
||||||
showInPageList = false
|
|
||||||
text = Markdown ""
|
|
||||||
revisions = []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// An entry in the list of pages displayed as part of the web log (derived via query)
|
|
||||||
type PageListEntry = {
|
|
||||||
/// The permanent link for the page
|
|
||||||
permalink : string
|
|
||||||
/// The title of the page
|
|
||||||
title : string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// A web log
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type WebLog = {
|
|
||||||
/// The Id
|
|
||||||
id : WebLogId
|
|
||||||
/// The name
|
|
||||||
name : string
|
|
||||||
/// The subtitle
|
|
||||||
subtitle : string option
|
|
||||||
/// The default page ("posts" or a page Id)
|
|
||||||
defaultPage : string
|
|
||||||
/// The path of the theme (within /views/themes)
|
|
||||||
themePath : string
|
|
||||||
/// The URL base
|
|
||||||
urlBase : string
|
|
||||||
/// The time zone in which dates/times should be displayed
|
|
||||||
timeZone : string
|
|
||||||
/// A list of pages to be rendered as part of the site navigation (not stored)
|
|
||||||
pageList : PageListEntry list
|
|
||||||
}
|
|
||||||
with
|
|
||||||
/// An empty web log
|
|
||||||
static member empty =
|
|
||||||
{ id = WebLogId.empty
|
|
||||||
name = ""
|
|
||||||
subtitle = None
|
|
||||||
defaultPage = ""
|
|
||||||
themePath = "default"
|
|
||||||
urlBase = ""
|
|
||||||
timeZone = "America/New_York"
|
|
||||||
pageList = []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// An authorization between a user and a web log
|
|
||||||
type Authorization = {
|
|
||||||
/// The Id of the web log to which this authorization grants access
|
|
||||||
webLogId : WebLogId
|
|
||||||
/// The level of access granted by this authorization
|
|
||||||
level : AuthorizationLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// A user of myWebLog
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type User = {
|
|
||||||
/// The Id
|
|
||||||
id : UserId
|
|
||||||
/// The user name (e-mail address)
|
|
||||||
userName : string
|
|
||||||
/// The first name
|
|
||||||
firstName : string
|
|
||||||
/// The last name
|
|
||||||
lastName : string
|
|
||||||
/// The user's preferred name
|
|
||||||
preferredName : string
|
|
||||||
/// The hash of the user's password
|
|
||||||
passwordHash : string
|
|
||||||
/// The URL of the user's personal site
|
|
||||||
url : string option
|
|
||||||
/// The user's authorizations
|
|
||||||
authorizations : Authorization list
|
|
||||||
}
|
|
||||||
with
|
|
||||||
/// An empty user
|
|
||||||
static member empty =
|
|
||||||
{ id = UserId.empty
|
|
||||||
userName = ""
|
|
||||||
firstName = ""
|
|
||||||
lastName = ""
|
|
||||||
preferredName = ""
|
|
||||||
passwordHash = ""
|
|
||||||
url = None
|
|
||||||
authorizations = []
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Functions supporting users
|
|
||||||
module User =
|
|
||||||
/// Claims for this user
|
|
||||||
let claims user =
|
|
||||||
user.authorizations
|
|
||||||
|> List.map (fun a -> sprintf "%s|%s" (WebLogId.toString a.webLogId) (AuthorizationLevel.toString a.level))
|
|
||||||
|
|
||||||
|
|
||||||
/// A category to which posts may be assigned
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type Category = {
|
|
||||||
/// The Id
|
|
||||||
id : CategoryId
|
|
||||||
/// The Id of the web log to which this category belongs
|
|
||||||
webLogId : WebLogId
|
|
||||||
/// The displayed name
|
|
||||||
name : string
|
|
||||||
/// The slug (used in category URLs)
|
|
||||||
slug : string
|
|
||||||
/// A longer description of the category
|
|
||||||
description : string option
|
|
||||||
/// The parent Id of this category (if a subcategory)
|
|
||||||
parentId : CategoryId option
|
|
||||||
/// The categories for which this category is the parent
|
|
||||||
children : CategoryId list
|
|
||||||
}
|
|
||||||
with
|
|
||||||
/// An empty category
|
|
||||||
static member empty =
|
|
||||||
{ id = CategoryId.empty
|
|
||||||
webLogId = WebLogId.empty
|
|
||||||
name = ""
|
|
||||||
slug = ""
|
|
||||||
description = None
|
|
||||||
parentId = None
|
|
||||||
children = []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// A comment (applies to a post)
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type Comment = {
|
|
||||||
/// The Id
|
|
||||||
id : CommentId
|
|
||||||
/// The Id of the post to which this comment applies
|
|
||||||
postId : PostId
|
|
||||||
/// The Id of the comment to which this comment is a reply
|
|
||||||
inReplyToId : CommentId option
|
|
||||||
/// The name of the commentor
|
|
||||||
name : string
|
|
||||||
/// The e-mail address of the commentor
|
|
||||||
email : string
|
|
||||||
/// The URL of the commentor's personal website
|
|
||||||
url : string option
|
|
||||||
/// The status of the comment
|
|
||||||
status : CommentStatus
|
|
||||||
/// The instant the comment was posted
|
|
||||||
postedOn : UnixSeconds
|
|
||||||
/// The text of the comment
|
|
||||||
text : string
|
|
||||||
}
|
|
||||||
with
|
|
||||||
static member empty =
|
|
||||||
{ id = CommentId.empty
|
|
||||||
postId = PostId.empty
|
|
||||||
inReplyToId = None
|
|
||||||
name = ""
|
|
||||||
email = ""
|
|
||||||
url = None
|
|
||||||
status = Pending
|
|
||||||
postedOn = UnixSeconds.none
|
|
||||||
text = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// A post
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type Post = {
|
|
||||||
/// The Id
|
|
||||||
id : PostId
|
|
||||||
/// The Id of the web log to which this post belongs
|
|
||||||
webLogId : WebLogId
|
|
||||||
/// The Id of the author of this post
|
|
||||||
authorId : UserId
|
|
||||||
/// The status
|
|
||||||
status : PostStatus
|
|
||||||
/// The title
|
|
||||||
title : string
|
|
||||||
/// The link at which the post resides
|
|
||||||
permalink : string
|
|
||||||
/// The instant on which the post was originally published
|
|
||||||
publishedOn : UnixSeconds
|
|
||||||
/// The instant on which the post was last updated
|
|
||||||
updatedOn : UnixSeconds
|
|
||||||
/// The text of the post
|
|
||||||
text : MarkupText
|
|
||||||
/// The Ids of the categories to which this is assigned
|
|
||||||
categoryIds : CategoryId list
|
|
||||||
/// The tags for the post
|
|
||||||
tags : string list
|
|
||||||
/// The permalinks at which this post may have once resided
|
|
||||||
priorPermalinks : string list
|
|
||||||
/// Revisions of this post
|
|
||||||
revisions : Revision list
|
|
||||||
/// The categories to which this is assigned (not stored in database)
|
|
||||||
categories : Category list
|
|
||||||
/// The comments (not stored in database)
|
|
||||||
comments : Comment list
|
|
||||||
}
|
|
||||||
with
|
|
||||||
static member empty =
|
|
||||||
{ id = PostId.empty
|
|
||||||
webLogId = WebLogId.empty
|
|
||||||
authorId = UserId.empty
|
|
||||||
status = Draft
|
|
||||||
title = ""
|
|
||||||
permalink = ""
|
|
||||||
publishedOn = UnixSeconds.none
|
|
||||||
updatedOn = UnixSeconds.none
|
|
||||||
text = Markdown ""
|
|
||||||
categoryIds = []
|
|
||||||
tags = []
|
|
||||||
priorPermalinks = []
|
|
||||||
revisions = []
|
|
||||||
categories = []
|
|
||||||
comments = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- UI Support ---
|
|
||||||
|
|
||||||
/// Counts of items displayed on the admin dashboard
|
|
||||||
type DashboardCounts = {
|
|
||||||
/// The number of pages for the web log
|
|
||||||
pages : int
|
|
||||||
/// The number of pages for the web log
|
|
||||||
posts : int
|
|
||||||
/// The number of categories for the web log
|
|
||||||
categories : int
|
|
||||||
}
|
|
67
src/MyWebLog/Features/FeatureSupport.cs
Normal file
67
src/MyWebLog/Features/FeatureSupport.cs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
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 _) { }
|
||||||
|
}
|
25
src/MyWebLog/Features/Posts/PostController.cs
Normal file
25
src/MyWebLog/Features/Posts/PostController.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace MyWebLog.Features.Posts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle post-related requests
|
||||||
|
/// </summary>
|
||||||
|
public class PostController : MyWebLogController
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public PostController(WebLogDbContext db) : base(db) { }
|
||||||
|
|
||||||
|
[HttpGet("~/")]
|
||||||
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
var webLog = WebLogCache.Get(HttpContext);
|
||||||
|
if (webLog.DefaultPage == "posts")
|
||||||
|
{
|
||||||
|
var posts = await Db.Posts.FindPageOfPublishedPosts(1, webLog.PostsPerPage);
|
||||||
|
return ThemedView("Index", posts);
|
||||||
|
}
|
||||||
|
var page = await Db.Pages.FindById(webLog.DefaultPage);
|
||||||
|
return page is null ? NotFound() : ThemedView("SinglePage", page);
|
||||||
|
}
|
||||||
|
}
|
25
src/MyWebLog/Features/Shared/MyWebLogController.cs
Normal file
25
src/MyWebLog/Features/Shared/MyWebLogController.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace MyWebLog.Features.Shared;
|
||||||
|
|
||||||
|
public abstract class MyWebLogController : Controller
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The data context to use to fulfil this request
|
||||||
|
/// </summary>
|
||||||
|
protected WebLogDbContext Db { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="db">The data context to use to fulfil this request</param>
|
||||||
|
protected MyWebLogController(WebLogDbContext db)
|
||||||
|
{
|
||||||
|
Db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ViewResult ThemedView(string template, object model)
|
||||||
|
{
|
||||||
|
return View(template, model);
|
||||||
|
}
|
||||||
|
}
|
28
src/MyWebLog/Features/ThemeSupport.cs
Normal file
28
src/MyWebLog/Features/ThemeSupport.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
33
src/MyWebLog/Features/Users/UserController.cs
Normal file
33
src/MyWebLog/Features/Users/UserController.cs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MyWebLog.Features.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for the users feature
|
||||||
|
/// </summary>
|
||||||
|
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 var alg = new Rfc2898DeriveBytes(plainText, allSalt, 2_048);
|
||||||
|
return Convert.ToBase64String(alg.GetBytes(64));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public UserController(WebLogDbContext db) : base(db) { }
|
||||||
|
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
}
|
2
src/MyWebLog/GlobalUsings.cs
Normal file
2
src/MyWebLog/GlobalUsings.cs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
global using MyWebLog.Data;
|
||||||
|
global using MyWebLog.Features.Shared;
|
24
src/MyWebLog/MyWebLog.csproj
Normal file
24
src/MyWebLog/MyWebLog.csproj
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Features\Admin\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<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.Data\MyWebLog.Data.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -1,28 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="Strings.fs" />
|
|
||||||
<Compile Include="Domain.fs" />
|
|
||||||
<Compile Include="Program.fs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Remove="Resources/en-US.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="Resources/en-US.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="DotLiquid" Version="2.2.548" />
|
|
||||||
<PackageReference Include="RethinkDb.Driver" Version="2.3.150" />
|
|
||||||
<PackageReference Include="Suave" Version="2.6.1" />
|
|
||||||
<PackageReference Include="Suave.DotLiquid" Version="2.6.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
131
src/MyWebLog/Program.cs
Normal file
131
src/MyWebLog/Program.cs
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MyWebLog;
|
||||||
|
using MyWebLog.Features;
|
||||||
|
using MyWebLog.Features.Users;
|
||||||
|
|
||||||
|
if (args.Length > 0 && args[0] == "init")
|
||||||
|
{
|
||||||
|
await InitDb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddMvc(opts => opts.Conventions.Add(new FeatureControllerModelConvention()))
|
||||||
|
.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!);
|
||||||
|
// "Data Source=Db/empty.db"
|
||||||
|
o.UseSqlite($"Data Source=Db/{db}.db");
|
||||||
|
});
|
||||||
|
|
||||||
|
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]}");
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
open MyWebLog
|
|
||||||
open Suave
|
|
||||||
|
|
||||||
startWebServer defaultConfig (Successful.OK (Strings.get "LastUpdated"))
|
|
28
src/MyWebLog/Properties/launchSettings.json
Normal file
28
src/MyWebLog/Properties/launchSettings.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"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,83 +0,0 @@
|
||||||
{
|
|
||||||
"Action": "Action",
|
|
||||||
"Added": "Added",
|
|
||||||
"AddNew": "Add New",
|
|
||||||
"AddNewCategory": "Add New Category",
|
|
||||||
"AddNewPage": "Add New Page",
|
|
||||||
"AddNewPost": "Add New Post",
|
|
||||||
"Admin": "Admin",
|
|
||||||
"AndPublished": " and Published",
|
|
||||||
"andXMore": "and {0} more...",
|
|
||||||
"at": "at",
|
|
||||||
"BackToCategoryList": "Back to Category List",
|
|
||||||
"BackToPageList": "Back to Page List",
|
|
||||||
"BackToPostList": "Back to Post List",
|
|
||||||
"Categories": "Categories",
|
|
||||||
"Category": "Category",
|
|
||||||
"CategoryDeleteWarning": "Are you sure you wish to delete the category",
|
|
||||||
"Close": "Close",
|
|
||||||
"Comments": "Comments",
|
|
||||||
"Dashboard": "Dashboard",
|
|
||||||
"Date": "Date",
|
|
||||||
"Delete": "Delete",
|
|
||||||
"Description": "Description",
|
|
||||||
"Edit": "Edit",
|
|
||||||
"EditCategory": "Edit Category",
|
|
||||||
"EditPage": "Edit Page",
|
|
||||||
"EditPost": "Edit Post",
|
|
||||||
"EmailAddress": "E-mail Address",
|
|
||||||
"ErrBadAppConfig": "Could not convert config.json to myWebLog configuration",
|
|
||||||
"ErrBadLogOnAttempt": "Invalid e-mail address or password",
|
|
||||||
"ErrDataConfig": "Could not convert data-config.json to RethinkDB connection",
|
|
||||||
"ErrNotConfigured": "is not properly configured for myWebLog",
|
|
||||||
"Error": "Error",
|
|
||||||
"LastUpdated": "Last Updated",
|
|
||||||
"LastUpdatedDate": "Last Updated Date",
|
|
||||||
"ListAll": "List All",
|
|
||||||
"LoadedIn": "Loaded in",
|
|
||||||
"LogOff": "Log Off",
|
|
||||||
"LogOn": "Log On",
|
|
||||||
"MsgCategoryDeleted": "Deleted category {0} successfully",
|
|
||||||
"MsgCategoryEditSuccess": "{0} category successfully",
|
|
||||||
"MsgLogOffSuccess": "Log off successful | Have a nice day!",
|
|
||||||
"MsgLogOnSuccess": "Log on successful | Welcome to myWebLog!",
|
|
||||||
"MsgPageDeleted": "Deleted page successfully",
|
|
||||||
"MsgPageEditSuccess": "{0} page successfully",
|
|
||||||
"MsgPostEditSuccess": "{0}{1} post successfully",
|
|
||||||
"Name": "Name",
|
|
||||||
"NewerPosts": "Newer Posts",
|
|
||||||
"NextPost": "Next Post",
|
|
||||||
"NoComments": "No Comments",
|
|
||||||
"NoParent": "No Parent",
|
|
||||||
"OlderPosts": "Older Posts",
|
|
||||||
"OneComment": "1 Comment",
|
|
||||||
"PageDeleteWarning": "Are you sure you wish to delete the page",
|
|
||||||
"PageDetails": "Page Details",
|
|
||||||
"PageHash": "Page #",
|
|
||||||
"Pages": "Pages",
|
|
||||||
"ParentCategory": "Parent Category",
|
|
||||||
"Password": "Password",
|
|
||||||
"Permalink": "Permalink",
|
|
||||||
"PermanentLinkTo": "Permanent Link to",
|
|
||||||
"PostDetails": "Post Details",
|
|
||||||
"Posts": "Posts",
|
|
||||||
"PostsTagged": "Posts Tagged",
|
|
||||||
"PostStatus": "Post Status",
|
|
||||||
"PoweredBy": "Powered by",
|
|
||||||
"PreviousPost": "Previous Post",
|
|
||||||
"PublishedDate": "Published Date",
|
|
||||||
"PublishThisPost": "Publish This Post",
|
|
||||||
"Save": "Save",
|
|
||||||
"Seconds": "Seconds",
|
|
||||||
"ShowInPageList": "Show in Page List",
|
|
||||||
"Slug": "Slug",
|
|
||||||
"startingWith": "starting with",
|
|
||||||
"Status": "Status",
|
|
||||||
"Tags": "Tags",
|
|
||||||
"Time": "Time",
|
|
||||||
"Title": "Title",
|
|
||||||
"Updated": "Updated",
|
|
||||||
"View": "View",
|
|
||||||
"Warning": "Warning",
|
|
||||||
"XComments": "{0} Comments"
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
module MyWebLog.Strings
|
|
||||||
|
|
||||||
open System.Collections.Generic
|
|
||||||
open System.Globalization
|
|
||||||
open System.IO
|
|
||||||
open System.Reflection
|
|
||||||
open System.Text.Json
|
|
||||||
|
|
||||||
/// The locales we'll try to load
|
|
||||||
let private supportedLocales = [ "en-US" ]
|
|
||||||
|
|
||||||
/// The fallback locale, if a key is not found in a non-default locale
|
|
||||||
let private fallbackLocale = "en-US"
|
|
||||||
|
|
||||||
/// Get an embedded JSON file as a string
|
|
||||||
let private getEmbedded locale =
|
|
||||||
let str = sprintf "MyWebLog.Resources.%s.json" locale |> Assembly.GetExecutingAssembly().GetManifestResourceStream
|
|
||||||
use rdr = new StreamReader (str)
|
|
||||||
rdr.ReadToEnd()
|
|
||||||
|
|
||||||
/// The dictionary of localized strings
|
|
||||||
let private strings =
|
|
||||||
supportedLocales
|
|
||||||
|> List.map (fun loc -> loc, getEmbedded loc |> JsonSerializer.Deserialize<Dictionary<string, string>>)
|
|
||||||
|> dict
|
|
||||||
|
|
||||||
/// Get a key from the resources file for the given locale
|
|
||||||
let getForLocale locale key =
|
|
||||||
let getString thisLocale =
|
|
||||||
match strings.ContainsKey thisLocale && strings.[thisLocale].ContainsKey key with
|
|
||||||
| true -> Some strings.[thisLocale].[key]
|
|
||||||
| false -> None
|
|
||||||
match getString locale with
|
|
||||||
| Some xlat -> Some xlat
|
|
||||||
| None when locale <> fallbackLocale -> getString fallbackLocale
|
|
||||||
| None -> None
|
|
||||||
|> function Some xlat -> xlat | None -> sprintf "%s.%s" locale key
|
|
||||||
|
|
||||||
/// Translate the key for the current locale
|
|
||||||
let get key = getForLocale CultureInfo.CurrentCulture.Name key
|
|
14
src/MyWebLog/Themes/Default/Shared/_Layout.cshtml
Normal file
14
src/MyWebLog/Themes/Default/Shared/_Layout.cshtml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
@inject IHttpContextAccessor ctxAcc
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<title>@ViewBag.Title « @WebLogCache.Get(ctxAcc.HttpContext!).Name</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
@RenderBody()
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
9
src/MyWebLog/Themes/Default/SinglePage.cshtml
Normal file
9
src/MyWebLog/Themes/Default/SinglePage.cshtml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
@model Page
|
||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewBag.Title = Model.Title;
|
||||||
|
}
|
||||||
|
<h2>@Model.Title</h2>
|
||||||
|
<article>
|
||||||
|
@Html.Raw(Model.Text)
|
||||||
|
</article>
|
1
src/MyWebLog/Themes/_ViewImports.cshtml
Normal file
1
src/MyWebLog/Themes/_ViewImports.cshtml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@namespace MyWebLog.Themes
|
90
src/MyWebLog/WebLogMiddleware.cs
Normal file
90
src/MyWebLog/WebLogMiddleware.cs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
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)) return;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
8
src/MyWebLog/appsettings.Development.json
Normal file
8
src/MyWebLog/appsettings.Development.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/MyWebLog/appsettings.json
Normal file
9
src/MyWebLog/appsettings.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user