WIP on DotLiquid support
This commit is contained in:
parent
62f7896621
commit
98eb2b1785
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@ -60,13 +60,14 @@ open Microsoft.FSharpLu.Json
|
||||
/// All converters to use for data conversion
|
||||
let all () : JsonConverter seq =
|
||||
seq {
|
||||
CategoryIdConverter ()
|
||||
CommentIdConverter ()
|
||||
PermalinkConverter ()
|
||||
PageIdConverter ()
|
||||
PostIdConverter ()
|
||||
WebLogIdConverter ()
|
||||
WebLogUserIdConverter ()
|
||||
// Our converters
|
||||
CategoryIdConverter ()
|
||||
CommentIdConverter ()
|
||||
PermalinkConverter ()
|
||||
PageIdConverter ()
|
||||
PostIdConverter ()
|
||||
WebLogIdConverter ()
|
||||
WebLogUserIdConverter ()
|
||||
// Handles DUs with no associated data, as well as option fields
|
||||
CompactUnionJsonConverter ()
|
||||
}
|
||||
|
@ -43,11 +43,19 @@ module Helpers =
|
||||
fun conn -> task {
|
||||
match! f conn with Some it when (prop it) = webLogId -> return Some it | _ -> return None
|
||||
}
|
||||
|
||||
/// Get the first item from a list, or None if the list is empty
|
||||
let tryFirst<'T> (f : IConnection -> Task<'T list>) =
|
||||
fun conn -> task {
|
||||
let! results = f conn
|
||||
return results |> List.tryHead
|
||||
}
|
||||
|
||||
|
||||
open RethinkDb.Driver.FSharp
|
||||
open Microsoft.Extensions.Logging
|
||||
|
||||
/// Start up checks to ensure the database, tables, and indexes exist
|
||||
module Startup =
|
||||
|
||||
/// Ensure field indexes exist, as well as special indexes for selected tables
|
||||
@ -151,6 +159,16 @@ module Category =
|
||||
/// Functions to manipulate pages
|
||||
module Page =
|
||||
|
||||
/// Add a new page
|
||||
let add (page : Page) =
|
||||
rethink {
|
||||
withTable Table.Page
|
||||
insert page
|
||||
write
|
||||
withRetryDefault
|
||||
ignoreResult
|
||||
}
|
||||
|
||||
/// Count all pages for a web log
|
||||
let countAll (webLogId : WebLogId) =
|
||||
rethink<int> {
|
||||
@ -195,14 +213,15 @@ module Page =
|
||||
|
||||
/// Find a page by its permalink
|
||||
let findByPermalink (permalink : Permalink) (webLogId : WebLogId) =
|
||||
rethink<Page> {
|
||||
rethink<Page list> {
|
||||
withTable Table.Page
|
||||
getAll [ r.Array (webLogId, permalink) ] (nameof permalink)
|
||||
without [ "priorPermalinks", "revisions" ]
|
||||
limit 1
|
||||
resultOption
|
||||
result
|
||||
withRetryDefault
|
||||
}
|
||||
|> tryFirst
|
||||
|
||||
/// Find a page by its ID (including permalinks and revisions)
|
||||
let findByFullId (pageId : PageId) webLogId =
|
||||
@ -243,14 +262,15 @@ module Post =
|
||||
|
||||
/// Find a post by its permalink
|
||||
let findByPermalink (permalink : Permalink) (webLogId : WebLogId) =
|
||||
rethink<Post> {
|
||||
rethink<Post list> {
|
||||
withTable Table.Post
|
||||
getAll [ r.Array(permalink, webLogId) ] (nameof permalink)
|
||||
without [ "priorPermalinks", "revisions" ]
|
||||
limit 1
|
||||
resultOption
|
||||
result
|
||||
withRetryDefault
|
||||
}
|
||||
|> tryFirst
|
||||
|
||||
/// Find posts to be displayed on a page
|
||||
let findPageOfPublishedPosts (webLogId : WebLogId) pageNbr postsPerPage =
|
||||
@ -270,15 +290,26 @@ module Post =
|
||||
/// Functions to manipulate web logs
|
||||
module WebLog =
|
||||
|
||||
/// Add a web log
|
||||
let add (webLog : WebLog) =
|
||||
rethink {
|
||||
withTable Table.WebLog
|
||||
insert webLog
|
||||
write
|
||||
withRetryOnce
|
||||
ignoreResult
|
||||
}
|
||||
|
||||
/// Retrieve web log details by the URL base
|
||||
let findByHost (url : string) =
|
||||
rethink<WebLog> {
|
||||
rethink<WebLog list> {
|
||||
withTable Table.WebLog
|
||||
getAll [ url ] "urlBase"
|
||||
limit 1
|
||||
resultOption
|
||||
result
|
||||
withRetryDefault
|
||||
}
|
||||
|> tryFirst
|
||||
|
||||
/// Update web log settings
|
||||
let updateSettings (webLog : WebLog) =
|
||||
@ -296,3 +327,18 @@ module WebLog =
|
||||
withRetryDefault
|
||||
ignoreResult
|
||||
}
|
||||
|
||||
|
||||
/// Functions to manipulate web log users
|
||||
module WebLogUser =
|
||||
|
||||
/// Add a web log user
|
||||
let add (user : WebLogUser) =
|
||||
rethink {
|
||||
withTable Table.WebLogUser
|
||||
insert user
|
||||
write
|
||||
withRetryDefault
|
||||
ignoreResult
|
||||
}
|
||||
|
@ -248,7 +248,7 @@ module WebLog =
|
||||
subtitle = None
|
||||
defaultPage = ""
|
||||
postsPerPage = 10
|
||||
themePath = "Default"
|
||||
themePath = "default"
|
||||
urlBase = ""
|
||||
timeZone = ""
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="SupportTypes.fs" />
|
||||
<Compile Include="DataTypes.fs" />
|
||||
<Compile Include="ViewModels.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -9,7 +9,7 @@ module private Helpers =
|
||||
/// Create a new ID (short GUID)
|
||||
// https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID
|
||||
let newId() =
|
||||
Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('/', '_').Replace('+', '-')[..22]
|
||||
Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('/', '_').Replace('+', '-').Substring (0, 22)
|
||||
|
||||
|
||||
/// An identifier for a category
|
||||
|
32
src/MyWebLog.Domain/ViewModels.fs
Normal file
32
src/MyWebLog.Domain/ViewModels.fs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace MyWebLog.ViewModels
|
||||
|
||||
open MyWebLog
|
||||
|
||||
/// Base model class for myWebLog views
|
||||
type MyWebLogModel (webLog : WebLog) =
|
||||
|
||||
/// The details for the web log
|
||||
member val WebLog = webLog with get
|
||||
|
||||
|
||||
/// The model to use to allow a user to log on
|
||||
[<CLIMutable>]
|
||||
type LogOnModel =
|
||||
{ /// The user's e-mail address
|
||||
emailAddress : string
|
||||
|
||||
/// The user's password
|
||||
password : string
|
||||
}
|
||||
|
||||
|
||||
/// The model used to render a single page
|
||||
type SinglePageModel =
|
||||
{ /// The page to be rendered
|
||||
page : Page
|
||||
|
||||
/// The web log to which the page belongs
|
||||
webLog : WebLog
|
||||
}
|
||||
/// Is this the home page?
|
||||
member this.isHome with get () = PageId.toString this.page.id = this.webLog.defaultPage
|
@ -20,7 +20,6 @@ type AdminController () =
|
||||
let! posts = Data.Post.countByStatus Published |> getCount
|
||||
let! drafts = Data.Post.countByStatus Draft |> getCount
|
||||
let! pages = Data.Page.countAll |> getCount
|
||||
let! pages = Data.Page.countAll |> getCount
|
||||
let! listed = Data.Page.countListed |> getCount
|
||||
let! cats = Data.Category.countAll |> getCount
|
||||
let! topCats = Data.Category.countTopLevel |> getCount
|
2
src/MyWebLog.FS.Old/Handlers.fs
Normal file
2
src/MyWebLog.FS.Old/Handlers.fs
Normal file
@ -0,0 +1,2 @@
|
||||
module MyWebLog.Handlers
|
||||
|
@ -5,6 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Handlers.fs" />
|
||||
<Compile Include="WebLogCache.fs" />
|
||||
<Compile Include="Features\Shared\SharedTypes.fs" />
|
||||
<Compile Include="Features\Admin\AdminTypes.fs" />
|
9
src/MyWebLog.FS.Old/appsettings.json
Normal file
9
src/MyWebLog.FS.Old/appsettings.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
@ -20,13 +20,6 @@
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyWebLog\MyWebLog.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
</Project>
|
||||
|
@ -3,17 +3,19 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32210.238
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog", "MyWebLog\MyWebLog.csproj", "{3139DA09-C999-465A-BC98-02FEC3BD7E88}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.Themes.BitBadger", "MyWebLog.Themes.BitBadger\MyWebLog.Themes.BitBadger.csproj", "{729F7AB3-2300-4390-B972-71D32FBBBF50}"
|
||||
EndProject
|
||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.FS", "MyWebLog.FS\MyWebLog.FS.fsproj", "{4D62F235-73BA-42A6-8AA1-29D0D046E115}"
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyWebLog.Domain", "MyWebLog.Domain\MyWebLog.Domain.fsproj", "{8CA99122-888A-4524-8C1B-685F0A4B7B4B}"
|
||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Domain", "MyWebLog.Domain\MyWebLog.Domain.fsproj", "{8CA99122-888A-4524-8C1B-685F0A4B7B4B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.DataCS", "MyWebLog.DataCS\MyWebLog.DataCS.csproj", "{C9129BED-E4AE-41BB-BDB2-5418B7F924CC}"
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyWebLog.Data", "MyWebLog.Data\MyWebLog.Data.fsproj", "{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}"
|
||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Data", "MyWebLog.Data\MyWebLog.Data.fsproj", "{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.CS", "MyWebLog.CS\MyWebLog.CS.csproj", "{B23A8093-28B1-4CB5-93F1-B4659516B74F}"
|
||||
EndProject
|
||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.FS.Old", "MyWebLog.FS.Old\MyWebLog.FS.Old.fsproj", "{C0AD7194-572E-4112-87C4-5235987C90C1}"
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyWebLog", "MyWebLog\MyWebLog.fsproj", "{5655B63D-429F-4CCD-A14C-FBD74D987ECB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -21,18 +23,10 @@ Global
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{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
|
||||
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4D62F235-73BA-42A6-8AA1-29D0D046E115}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4D62F235-73BA-42A6-8AA1-29D0D046E115}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4D62F235-73BA-42A6-8AA1-29D0D046E115}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4D62F235-73BA-42A6-8AA1-29D0D046E115}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@ -45,6 +39,18 @@ Global
|
||||
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B23A8093-28B1-4CB5-93F1-B4659516B74F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B23A8093-28B1-4CB5-93F1-B4659516B74F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B23A8093-28B1-4CB5-93F1-B4659516B74F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B23A8093-28B1-4CB5-93F1-B4659516B74F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C0AD7194-572E-4112-87C4-5235987C90C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C0AD7194-572E-4112-87C4-5235987C90C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C0AD7194-572E-4112-87C4-5235987C90C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C0AD7194-572E-4112-87C4-5235987C90C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
59
src/MyWebLog/Handlers.fs
Normal file
59
src/MyWebLog/Handlers.fs
Normal file
@ -0,0 +1,59 @@
|
||||
[<RequireQualifiedAccess>]
|
||||
module MyWebLog.Handlers
|
||||
|
||||
open Giraffe
|
||||
open MyWebLog
|
||||
open MyWebLog.ViewModels
|
||||
open System
|
||||
|
||||
[<AutoOpen>]
|
||||
module private Helpers =
|
||||
|
||||
open DotLiquid
|
||||
open System.Collections.Concurrent
|
||||
open System.IO
|
||||
|
||||
/// Cache for parsed templates
|
||||
let private themeViews = ConcurrentDictionary<string, Template> ()
|
||||
|
||||
/// Return a view for a theme
|
||||
let themedView<'T> (template : string) (model : obj) : HttpHandler = fun next ctx -> task {
|
||||
let webLog = WebLogCache.getByCtx ctx
|
||||
let templatePath = $"themes/{webLog.themePath}/{template}"
|
||||
match themeViews.ContainsKey templatePath with
|
||||
| true -> ()
|
||||
| false ->
|
||||
let! file = File.ReadAllTextAsync $"{templatePath}.liquid"
|
||||
themeViews[templatePath] <- Template.Parse file
|
||||
let view = themeViews[templatePath].Render (Hash.FromAnonymousObject model)
|
||||
return! htmlString view next ctx
|
||||
}
|
||||
|
||||
module User =
|
||||
|
||||
open System.Security.Cryptography
|
||||
open System.Text
|
||||
|
||||
/// Hash a password for a given user
|
||||
let hashedPassword (plainText : string) (email : string) (salt : Guid) =
|
||||
let allSalt = Array.concat [ salt.ToByteArray(); (Encoding.UTF8.GetBytes email) ]
|
||||
use alg = new Rfc2898DeriveBytes (plainText, allSalt, 2_048)
|
||||
Convert.ToBase64String(alg.GetBytes(64))
|
||||
|
||||
|
||||
module CatchAll =
|
||||
|
||||
let catchAll : HttpHandler = fun next ctx -> task {
|
||||
let testPage = { Page.empty with text = "Howdy, folks!" }
|
||||
return! themedView "single-page" { page = testPage; webLog = WebLogCache.getByCtx ctx } next ctx
|
||||
}
|
||||
|
||||
open Giraffe.EndpointRouting
|
||||
|
||||
/// The endpoints defined in the above handlers
|
||||
let endpoints = [
|
||||
GET [
|
||||
route "" CatchAll.catchAll
|
||||
]
|
||||
]
|
||||
|
32
src/MyWebLog/MyWebLog.fsproj
Normal file
32
src/MyWebLog/MyWebLog.fsproj
Normal file
@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json" CopyToOutputDirectory="Always" />
|
||||
<Compile Include="WebLogCache.fs" />
|
||||
<Compile Include="Handlers.fs" />
|
||||
<Compile Include="Program.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotLiquid" Version="2.2.610" />
|
||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyWebLog.Data\MyWebLog.Data.fsproj" />
|
||||
<ProjectReference Include="..\MyWebLog.Domain\MyWebLog.Domain.fsproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include=".\themes\**" CopyToOutputDirectory="Always" />
|
||||
<None Include=".\wwwroot\**" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup />
|
||||
|
||||
</Project>
|
150
src/MyWebLog/Program.fs
Normal file
150
src/MyWebLog/Program.fs
Normal file
@ -0,0 +1,150 @@
|
||||
open Giraffe.EndpointRouting
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.Extensions.Configuration
|
||||
open Microsoft.Extensions.Hosting
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Microsoft.Extensions.Logging
|
||||
open MyWebLog
|
||||
open RethinkDb.Driver.FSharp
|
||||
open RethinkDb.Driver.Net
|
||||
open System
|
||||
|
||||
/// Middleware to derive the current web log
|
||||
type WebLogMiddleware (next : RequestDelegate) =
|
||||
|
||||
member this.InvokeAsync (ctx : HttpContext) = task {
|
||||
let host = ctx.Request.Host.ToUriComponent ()
|
||||
match WebLogCache.exists host with
|
||||
| true -> return! next.Invoke ctx
|
||||
| false ->
|
||||
let conn = ctx.RequestServices.GetRequiredService<IConnection> ()
|
||||
match! Data.WebLog.findByHost host conn with
|
||||
| Some webLog ->
|
||||
WebLogCache.set host webLog
|
||||
return! next.Invoke ctx
|
||||
| None -> ctx.Response.StatusCode <- 404
|
||||
}
|
||||
|
||||
|
||||
/// Initialize a new database
|
||||
let initDbValidated (args : string[]) (sp : IServiceProvider) = task {
|
||||
|
||||
let conn = sp.GetRequiredService<IConnection> ()
|
||||
|
||||
let timeZone =
|
||||
let local = TimeZoneInfo.Local.Id
|
||||
match TimeZoneInfo.Local.HasIanaId with
|
||||
| true -> local
|
||||
| false ->
|
||||
match TimeZoneInfo.TryConvertWindowsIdToIanaId local with
|
||||
| true, ianaId -> ianaId
|
||||
| false, _ -> raise <| TimeZoneNotFoundException $"Cannot find IANA timezone for {local}"
|
||||
|
||||
// Create the web log
|
||||
let webLogId = WebLogId.create ()
|
||||
let userId = WebLogUserId.create ()
|
||||
let homePageId = PageId.create ()
|
||||
|
||||
do! Data.WebLog.add
|
||||
{ WebLog.empty with
|
||||
id = webLogId
|
||||
name = args[2]
|
||||
urlBase = args[1]
|
||||
defaultPage = PageId.toString homePageId
|
||||
timeZone = timeZone
|
||||
} conn
|
||||
|
||||
// Create the admin user
|
||||
let salt = Guid.NewGuid ()
|
||||
|
||||
do! Data.WebLogUser.add
|
||||
{ WebLogUser.empty with
|
||||
id = userId
|
||||
webLogId = webLogId
|
||||
userName = args[3]
|
||||
firstName = "Admin"
|
||||
lastName = "User"
|
||||
preferredName = "Admin"
|
||||
passwordHash = Handlers.User.hashedPassword args[4] args[3] salt
|
||||
salt = salt
|
||||
authorizationLevel = Administrator
|
||||
} conn
|
||||
|
||||
// Create the default home page
|
||||
do! Data.Page.add
|
||||
{ Page.empty with
|
||||
id = homePageId
|
||||
webLogId = webLogId
|
||||
authorId = userId
|
||||
title = "Welcome to myWebLog!"
|
||||
permalink = Permalink "welcome-to-myweblog.html"
|
||||
publishedOn = DateTime.UtcNow
|
||||
updatedOn = DateTime.UtcNow
|
||||
text = "<p>This is your default home page.</p>"
|
||||
revisions = [
|
||||
{ asOf = DateTime.UtcNow
|
||||
sourceType = Html
|
||||
text = "<p>This is your default home page.</p>"
|
||||
}
|
||||
]
|
||||
} conn
|
||||
|
||||
Console.WriteLine($"Successfully initialized database for {args[2]} with URL base {args[1]}");
|
||||
}
|
||||
|
||||
/// Initialize a new database
|
||||
let initDb args sp = task {
|
||||
match args |> Array.length with
|
||||
| 5 -> return! initDbValidated args sp
|
||||
| _ ->
|
||||
Console.WriteLine "Usage: MyWebLog init [url] [name] [admin-email] [admin-pw]"
|
||||
return! System.Threading.Tasks.Task.CompletedTask
|
||||
}
|
||||
|
||||
|
||||
[<EntryPoint>]
|
||||
let main args =
|
||||
|
||||
let builder = WebApplication.CreateBuilder(args)
|
||||
let _ =
|
||||
builder.Services
|
||||
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(fun opts ->
|
||||
opts.ExpireTimeSpan <- TimeSpan.FromMinutes 20.
|
||||
opts.SlidingExpiration <- true
|
||||
opts.AccessDeniedPath <- "/forbidden")
|
||||
let _ = builder.Services.AddLogging ()
|
||||
let _ = builder.Services.AddAuthorization()
|
||||
|
||||
// Configure RethinkDB's connection
|
||||
JsonConverters.all () |> Seq.iter Converter.Serializer.Converters.Add
|
||||
let sp = builder.Services.BuildServiceProvider ()
|
||||
let config = sp.GetRequiredService<IConfiguration> ()
|
||||
let loggerFac = sp.GetRequiredService<ILoggerFactory> ()
|
||||
let rethinkCfg = DataConfig.FromConfiguration (config.GetSection "RethinkDB")
|
||||
let conn =
|
||||
task {
|
||||
let! conn = rethinkCfg.CreateConnectionAsync ()
|
||||
do! Data.Startup.ensureDb rethinkCfg (loggerFac.CreateLogger (nameof Data.Startup)) conn
|
||||
return conn
|
||||
} |> Async.AwaitTask |> Async.RunSynchronously
|
||||
let _ = builder.Services.AddSingleton<IConnection> conn
|
||||
|
||||
let app = builder.Build ()
|
||||
|
||||
match args |> Array.tryHead with
|
||||
| Some it when it = "init" -> initDb args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
||||
| _ ->
|
||||
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
||||
let _ = app.UseMiddleware<WebLogMiddleware> ()
|
||||
let _ = app.UseAuthentication ()
|
||||
let _ = app.UseStaticFiles ()
|
||||
let _ = app.UseRouting ()
|
||||
let _ = app.UseEndpoints (fun endpoints -> endpoints.MapGiraffeEndpoints Handlers.endpoints)
|
||||
|
||||
app.Run()
|
||||
|
||||
0 // Exit code
|
||||
|
24
src/MyWebLog/WebLogCache.fs
Normal file
24
src/MyWebLog/WebLogCache.fs
Normal file
@ -0,0 +1,24 @@
|
||||
/// <summary>
|
||||
/// In-memory cache of web log details
|
||||
/// </summary>
|
||||
/// <remarks>This is filled by the middleware via the first request for each host, and can be updated via the web log
|
||||
/// settings update page</remarks>
|
||||
module MyWebLog.WebLogCache
|
||||
|
||||
open Microsoft.AspNetCore.Http
|
||||
open System.Collections.Concurrent
|
||||
|
||||
/// The cache of web log details
|
||||
let private _cache = ConcurrentDictionary<string, WebLog> ()
|
||||
|
||||
/// Does a host exist in the cache?
|
||||
let exists host = _cache.ContainsKey host
|
||||
|
||||
/// Get the details for a web log via its host
|
||||
let get host = _cache[host]
|
||||
|
||||
/// Get the details for a web log via its host
|
||||
let getByCtx (ctx : HttpContext) = _cache[ctx.Request.Host.ToUriComponent ()]
|
||||
|
||||
/// Set the details for a particular host
|
||||
let set host details = _cache[host] <- details
|
@ -1,9 +1,6 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
{
|
||||
"RethinkDB": {
|
||||
"hostname": "data02.bitbadger.solutions",
|
||||
"database": "myWebLog-dev"
|
||||
}
|
||||
}
|
||||
|
11
src/MyWebLog/themes/default/_html-head.liquid
Normal file
11
src/MyWebLog/themes/default/_html-head.liquid
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="generator" content="myWebLog 2">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link asp-theme="@Model.WebLog.ThemePath" />
|
||||
<title>{{ title | escape }} « {{ web_log_name | escape }}</title>
|
||||
</head>
|
6
src/MyWebLog/themes/default/_page-foot.liquid
Normal file
6
src/MyWebLog/themes/default/_page-foot.liquid
Normal file
@ -0,0 +1,6 @@
|
||||
<footer>
|
||||
<hr>
|
||||
<div class="container-fluid text-end">
|
||||
<img src="/img/logo-dark.png" alt="myWebLog">
|
||||
</div>
|
||||
</footer>
|
18
src/MyWebLog/themes/default/_page-head.liquid
Normal file
18
src/MyWebLog/themes/default/_page-head.liquid
Normal file
@ -0,0 +1,18 @@
|
||||
<header>
|
||||
<nav class="navbar navbar-light bg-light navbar-expand-md justify-content-start px-2">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="~/">{{ web_log.name }}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
{% if web_log.subtitle -%}
|
||||
<span class="navbar-text">{{ web_log.subtitle | escape }}</span>
|
||||
{%- endif %}
|
||||
@* TODO: list pages for current web log *@
|
||||
@await Html.PartialAsync("_LogOnOffPartial")
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
14
src/MyWebLog/themes/default/single-page.liquid
Normal file
14
src/MyWebLog/themes/default/single-page.liquid
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{ render "_html-head", title: title, web_log_name: web_log.name }}
|
||||
<body>
|
||||
{{ render "_page-head", web_log: web_log }}
|
||||
<main>
|
||||
<h2>{{ page.title }}</h2>
|
||||
<article>
|
||||
{{ page.text }}
|
||||
</article>
|
||||
</main>
|
||||
{{ render "_page-foot" }}
|
||||
</body>
|
||||
</html>
|
5
src/MyWebLog/wwwroot/admin/admin.css
Normal file
5
src/MyWebLog/wwwroot/admin/admin.css
Normal file
@ -0,0 +1,5 @@
|
||||
footer {
|
||||
background-color: #808080;
|
||||
border-top: solid 1px black;
|
||||
color: white;
|
||||
}
|
Loading…
Reference in New Issue
Block a user