V2 #1
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.0 KiB |
|
@ -60,13 +60,14 @@ open Microsoft.FSharpLu.Json
|
||||||
/// All converters to use for data conversion
|
/// All converters to use for data conversion
|
||||||
let all () : JsonConverter seq =
|
let all () : JsonConverter seq =
|
||||||
seq {
|
seq {
|
||||||
CategoryIdConverter ()
|
// Our converters
|
||||||
CommentIdConverter ()
|
CategoryIdConverter ()
|
||||||
PermalinkConverter ()
|
CommentIdConverter ()
|
||||||
PageIdConverter ()
|
PermalinkConverter ()
|
||||||
PostIdConverter ()
|
PageIdConverter ()
|
||||||
WebLogIdConverter ()
|
PostIdConverter ()
|
||||||
WebLogUserIdConverter ()
|
WebLogIdConverter ()
|
||||||
|
WebLogUserIdConverter ()
|
||||||
// Handles DUs with no associated data, as well as option fields
|
// Handles DUs with no associated data, as well as option fields
|
||||||
CompactUnionJsonConverter ()
|
CompactUnionJsonConverter ()
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,10 +44,18 @@ module Helpers =
|
||||||
match! f conn with Some it when (prop it) = webLogId -> return Some it | _ -> return None
|
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 RethinkDb.Driver.FSharp
|
||||||
open Microsoft.Extensions.Logging
|
open Microsoft.Extensions.Logging
|
||||||
|
|
||||||
|
/// Start up checks to ensure the database, tables, and indexes exist
|
||||||
module Startup =
|
module Startup =
|
||||||
|
|
||||||
/// Ensure field indexes exist, as well as special indexes for selected tables
|
/// Ensure field indexes exist, as well as special indexes for selected tables
|
||||||
|
@ -151,6 +159,16 @@ module Category =
|
||||||
/// Functions to manipulate pages
|
/// Functions to manipulate pages
|
||||||
module Page =
|
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
|
/// Count all pages for a web log
|
||||||
let countAll (webLogId : WebLogId) =
|
let countAll (webLogId : WebLogId) =
|
||||||
rethink<int> {
|
rethink<int> {
|
||||||
|
@ -195,14 +213,15 @@ module Page =
|
||||||
|
|
||||||
/// Find a page by its permalink
|
/// Find a page by its permalink
|
||||||
let findByPermalink (permalink : Permalink) (webLogId : WebLogId) =
|
let findByPermalink (permalink : Permalink) (webLogId : WebLogId) =
|
||||||
rethink<Page> {
|
rethink<Page list> {
|
||||||
withTable Table.Page
|
withTable Table.Page
|
||||||
getAll [ r.Array (webLogId, permalink) ] (nameof permalink)
|
getAll [ r.Array (webLogId, permalink) ] (nameof permalink)
|
||||||
without [ "priorPermalinks", "revisions" ]
|
without [ "priorPermalinks", "revisions" ]
|
||||||
limit 1
|
limit 1
|
||||||
resultOption
|
result
|
||||||
withRetryDefault
|
withRetryDefault
|
||||||
}
|
}
|
||||||
|
|> tryFirst
|
||||||
|
|
||||||
/// Find a page by its ID (including permalinks and revisions)
|
/// Find a page by its ID (including permalinks and revisions)
|
||||||
let findByFullId (pageId : PageId) webLogId =
|
let findByFullId (pageId : PageId) webLogId =
|
||||||
|
@ -243,14 +262,15 @@ module Post =
|
||||||
|
|
||||||
/// Find a post by its permalink
|
/// Find a post by its permalink
|
||||||
let findByPermalink (permalink : Permalink) (webLogId : WebLogId) =
|
let findByPermalink (permalink : Permalink) (webLogId : WebLogId) =
|
||||||
rethink<Post> {
|
rethink<Post list> {
|
||||||
withTable Table.Post
|
withTable Table.Post
|
||||||
getAll [ r.Array(permalink, webLogId) ] (nameof permalink)
|
getAll [ r.Array(permalink, webLogId) ] (nameof permalink)
|
||||||
without [ "priorPermalinks", "revisions" ]
|
without [ "priorPermalinks", "revisions" ]
|
||||||
limit 1
|
limit 1
|
||||||
resultOption
|
result
|
||||||
withRetryDefault
|
withRetryDefault
|
||||||
}
|
}
|
||||||
|
|> tryFirst
|
||||||
|
|
||||||
/// Find posts to be displayed on a page
|
/// Find posts to be displayed on a page
|
||||||
let findPageOfPublishedPosts (webLogId : WebLogId) pageNbr postsPerPage =
|
let findPageOfPublishedPosts (webLogId : WebLogId) pageNbr postsPerPage =
|
||||||
|
@ -270,15 +290,26 @@ module Post =
|
||||||
/// Functions to manipulate web logs
|
/// Functions to manipulate web logs
|
||||||
module WebLog =
|
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
|
/// Retrieve web log details by the URL base
|
||||||
let findByHost (url : string) =
|
let findByHost (url : string) =
|
||||||
rethink<WebLog> {
|
rethink<WebLog list> {
|
||||||
withTable Table.WebLog
|
withTable Table.WebLog
|
||||||
getAll [ url ] "urlBase"
|
getAll [ url ] "urlBase"
|
||||||
limit 1
|
limit 1
|
||||||
resultOption
|
result
|
||||||
withRetryDefault
|
withRetryDefault
|
||||||
}
|
}
|
||||||
|
|> tryFirst
|
||||||
|
|
||||||
/// Update web log settings
|
/// Update web log settings
|
||||||
let updateSettings (webLog : WebLog) =
|
let updateSettings (webLog : WebLog) =
|
||||||
|
@ -296,3 +327,18 @@ module WebLog =
|
||||||
withRetryDefault
|
withRetryDefault
|
||||||
ignoreResult
|
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
|
subtitle = None
|
||||||
defaultPage = ""
|
defaultPage = ""
|
||||||
postsPerPage = 10
|
postsPerPage = 10
|
||||||
themePath = "Default"
|
themePath = "default"
|
||||||
urlBase = ""
|
urlBase = ""
|
||||||
timeZone = ""
|
timeZone = ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="SupportTypes.fs" />
|
<Compile Include="SupportTypes.fs" />
|
||||||
<Compile Include="DataTypes.fs" />
|
<Compile Include="DataTypes.fs" />
|
||||||
|
<Compile Include="ViewModels.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -9,7 +9,7 @@ module private Helpers =
|
||||||
/// Create a new ID (short GUID)
|
/// Create a new ID (short GUID)
|
||||||
// https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID
|
// https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID
|
||||||
let newId() =
|
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
|
/// 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! posts = Data.Post.countByStatus Published |> getCount
|
||||||
let! drafts = Data.Post.countByStatus Draft |> getCount
|
let! drafts = Data.Post.countByStatus Draft |> getCount
|
||||||
let! pages = Data.Page.countAll |> getCount
|
let! pages = Data.Page.countAll |> getCount
|
||||||
let! pages = Data.Page.countAll |> getCount
|
|
||||||
let! listed = Data.Page.countListed |> getCount
|
let! listed = Data.Page.countListed |> getCount
|
||||||
let! cats = Data.Category.countAll |> getCount
|
let! cats = Data.Category.countAll |> getCount
|
||||||
let! topCats = Data.Category.countTopLevel |> 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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="Handlers.fs" />
|
||||||
<Compile Include="WebLogCache.fs" />
|
<Compile Include="WebLogCache.fs" />
|
||||||
<Compile Include="Features\Shared\SharedTypes.fs" />
|
<Compile Include="Features\Shared\SharedTypes.fs" />
|
||||||
<Compile Include="Features\Admin\AdminTypes.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" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MyWebLog\MyWebLog.csproj">
|
|
||||||
<Private>false</Private>
|
|
||||||
<ExcludeAssets>runtime</ExcludeAssets>
|
|
||||||
</ProjectReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -3,17 +3,19 @@ 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("{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}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.Themes.BitBadger", "MyWebLog.Themes.BitBadger\MyWebLog.Themes.BitBadger.csproj", "{729F7AB3-2300-4390-B972-71D32FBBBF50}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.FS", "MyWebLog.FS\MyWebLog.FS.fsproj", "{4D62F235-73BA-42A6-8AA1-29D0D046E115}"
|
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Domain", "MyWebLog.Domain\MyWebLog.Domain.fsproj", "{8CA99122-888A-4524-8C1B-685F0A4B7B4B}"
|
||||||
EndProject
|
|
||||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyWebLog.Domain", "MyWebLog.Domain\MyWebLog.Domain.fsproj", "{8CA99122-888A-4524-8C1B-685F0A4B7B4B}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.DataCS", "MyWebLog.DataCS\MyWebLog.DataCS.csproj", "{C9129BED-E4AE-41BB-BDB2-5418B7F924CC}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyWebLog.DataCS", "MyWebLog.DataCS\MyWebLog.DataCS.csproj", "{C9129BED-E4AE-41BB-BDB2-5418B7F924CC}"
|
||||||
EndProject
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
@ -21,18 +23,10 @@ Global
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
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.ActiveCfg = Debug|Any CPU
|
||||||
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{729F7AB3-2300-4390-B972-71D32FBBBF50}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
{8CA99122-888A-4524-8C1B-685F0A4B7B4B}.Debug|Any CPU.Build.0 = 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
|
{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}.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.ActiveCfg = Release|Any CPU
|
||||||
{D284584D-2CB2-40C8-B605-6D0FD84D9D3D}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
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": {
|
"RethinkDB": {
|
||||||
"LogLevel": {
|
"hostname": "data02.bitbadger.solutions",
|
||||||
"Default": "Information",
|
"database": "myWebLog-dev"
|
||||||
"Microsoft.AspNetCore": "Warning"
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
}
|
||||||
|
|
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