2022-05-22 02:55:13 +00:00
|
|
|
open Microsoft.AspNetCore.Http
|
2022-05-22 22:59:48 +00:00
|
|
|
open Microsoft.Extensions.Logging
|
2022-04-17 03:06:38 +00:00
|
|
|
open MyWebLog
|
|
|
|
|
|
|
|
/// Middleware to derive the current web log
|
2022-05-22 22:59:48 +00:00
|
|
|
type WebLogMiddleware (next : RequestDelegate, log : ILogger<WebLogMiddleware>) =
|
|
|
|
|
|
|
|
/// Is the debug level enabled on the logger?
|
|
|
|
let isDebug = log.IsEnabled LogLevel.Debug
|
|
|
|
|
2022-04-17 03:06:38 +00:00
|
|
|
member this.InvokeAsync (ctx : HttpContext) = task {
|
2022-05-22 22:59:48 +00:00
|
|
|
/// Create the full path of the request
|
|
|
|
let path = $"{ctx.Request.Scheme}://{ctx.Request.Host.Value}{ctx.Request.Path.Value}"
|
|
|
|
match WebLogCache.tryGet path with
|
2022-05-22 22:24:09 +00:00
|
|
|
| Some webLog ->
|
2022-05-22 22:59:48 +00:00
|
|
|
if isDebug then log.LogDebug $"Resolved web log {WebLogId.toString webLog.id} for {path}"
|
2022-05-22 22:24:09 +00:00
|
|
|
ctx.Items["webLog"] <- webLog
|
2022-05-22 02:55:13 +00:00
|
|
|
if PageListCache.exists ctx then () else do! PageListCache.update ctx
|
|
|
|
if CategoryCache.exists ctx then () else do! CategoryCache.update ctx
|
|
|
|
return! next.Invoke ctx
|
2022-05-22 22:59:48 +00:00
|
|
|
| None ->
|
|
|
|
if isDebug then log.LogDebug $"No resolved web log for {path}"
|
|
|
|
ctx.Response.StatusCode <- 404
|
2022-04-17 03:06:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-05-22 02:55:13 +00:00
|
|
|
open System
|
|
|
|
open Microsoft.Extensions.DependencyInjection
|
|
|
|
open RethinkDb.Driver.Net
|
2022-04-18 22:06:17 +00:00
|
|
|
|
2022-04-19 20:25:51 +00:00
|
|
|
/// Create the default information for a new web log
|
|
|
|
module NewWebLog =
|
2022-04-17 03:06:38 +00:00
|
|
|
|
2022-04-30 03:46:26 +00:00
|
|
|
open System.IO
|
|
|
|
open RethinkDb.Driver.FSharp
|
|
|
|
|
2022-04-19 20:25:51 +00:00
|
|
|
/// Create the web log information
|
|
|
|
let private createWebLog (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
|
|
|
|
text = Html "<p>This is your default home page.</p>"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
} conn
|
|
|
|
|
2022-04-30 03:46:26 +00:00
|
|
|
printfn $"Successfully initialized database for {args[2]} with URL base {args[1]}"
|
2022-04-19 20:25:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a new web log
|
|
|
|
let create args sp = task {
|
|
|
|
match args |> Array.length with
|
|
|
|
| 5 -> return! createWebLog args sp
|
|
|
|
| _ ->
|
2022-04-30 03:46:26 +00:00
|
|
|
printfn "Usage: MyWebLog init [url] [name] [admin-email] [admin-pw]"
|
|
|
|
return! System.Threading.Tasks.Task.CompletedTask
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Import prior permalinks from a text files with lines in the format "[old] [new]"
|
|
|
|
let importPriorPermalinks urlBase file (sp : IServiceProvider) = task {
|
|
|
|
let conn = sp.GetRequiredService<IConnection> ()
|
|
|
|
|
|
|
|
match! Data.WebLog.findByHost urlBase conn with
|
|
|
|
| Some webLog ->
|
|
|
|
|
|
|
|
let mapping =
|
|
|
|
File.ReadAllLines file
|
|
|
|
|> Seq.ofArray
|
|
|
|
|> Seq.map (fun it ->
|
|
|
|
let parts = it.Split " "
|
|
|
|
Permalink parts[0], Permalink parts[1])
|
|
|
|
|
|
|
|
for old, current in mapping do
|
|
|
|
match! Data.Post.findByPermalink current webLog.id conn with
|
|
|
|
| Some post ->
|
|
|
|
let! withLinks = rethink<Post> {
|
|
|
|
withTable Data.Table.Post
|
|
|
|
get post.id
|
|
|
|
result conn
|
|
|
|
}
|
|
|
|
do! rethink {
|
|
|
|
withTable Data.Table.Post
|
|
|
|
get post.id
|
|
|
|
update [ "priorPermalinks", old :: withLinks.priorPermalinks :> obj]
|
|
|
|
write; ignoreResult conn
|
|
|
|
}
|
|
|
|
printfn $"{Permalink.toString old} -> {Permalink.toString current}"
|
|
|
|
| None -> printfn $"Cannot find current post for {Permalink.toString current}"
|
|
|
|
printfn "Done!"
|
|
|
|
| None -> printfn $"No web log found at {urlBase}"
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Import permalinks if all is well
|
|
|
|
let importPermalinks args sp = task {
|
|
|
|
match args |> Array.length with
|
|
|
|
| 3 -> return! importPriorPermalinks args[1] args[2] sp
|
|
|
|
| _ ->
|
|
|
|
printfn "Usage: MyWebLog import-permalinks [url] [file-name]"
|
2022-04-19 20:25:51 +00:00
|
|
|
return! System.Threading.Tasks.Task.CompletedTask
|
|
|
|
}
|
2022-04-17 03:06:38 +00:00
|
|
|
|
2022-04-18 05:05:06 +00:00
|
|
|
|
2022-05-22 02:55:13 +00:00
|
|
|
open System.Collections.Generic
|
2022-04-18 01:30:00 +00:00
|
|
|
open DotLiquid
|
2022-05-22 02:55:13 +00:00
|
|
|
open DotLiquidBespoke
|
2022-04-18 05:05:06 +00:00
|
|
|
open Giraffe
|
|
|
|
open Giraffe.EndpointRouting
|
|
|
|
open Microsoft.AspNetCore.Antiforgery
|
|
|
|
open Microsoft.AspNetCore.Authentication.Cookies
|
|
|
|
open Microsoft.AspNetCore.Builder
|
2022-05-22 22:24:09 +00:00
|
|
|
open Microsoft.AspNetCore.HttpOverrides
|
2022-04-18 05:05:06 +00:00
|
|
|
open Microsoft.Extensions.Configuration
|
2022-04-18 01:30:00 +00:00
|
|
|
open MyWebLog.ViewModels
|
2022-04-19 20:25:51 +00:00
|
|
|
open RethinkDB.DistributedCache
|
2022-04-18 05:05:06 +00:00
|
|
|
open RethinkDb.Driver.FSharp
|
2022-04-17 03:06:38 +00:00
|
|
|
|
|
|
|
[<EntryPoint>]
|
|
|
|
let main args =
|
|
|
|
|
|
|
|
let builder = WebApplication.CreateBuilder(args)
|
2022-05-22 22:24:09 +00:00
|
|
|
let _ = builder.Services.Configure<ForwardedHeadersOptions>(fun (opts : ForwardedHeadersOptions) ->
|
|
|
|
opts.ForwardedHeaders <- ForwardedHeaders.XForwardedFor ||| ForwardedHeaders.XForwardedProto)
|
2022-04-17 03:06:38 +00:00
|
|
|
let _ =
|
|
|
|
builder.Services
|
|
|
|
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
|
|
.AddCookie(fun opts ->
|
2022-04-25 17:36:16 +00:00
|
|
|
opts.ExpireTimeSpan <- TimeSpan.FromMinutes 60.
|
2022-04-17 03:06:38 +00:00
|
|
|
opts.SlidingExpiration <- true
|
|
|
|
opts.AccessDeniedPath <- "/forbidden")
|
|
|
|
let _ = builder.Services.AddLogging ()
|
2022-04-18 05:05:06 +00:00
|
|
|
let _ = builder.Services.AddAuthorization ()
|
|
|
|
let _ = builder.Services.AddAntiforgery ()
|
2022-04-17 03:06:38 +00:00
|
|
|
|
|
|
|
// 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
|
2022-05-22 02:55:13 +00:00
|
|
|
do! WebLogCache.fill conn
|
2022-04-17 03:06:38 +00:00
|
|
|
return conn
|
|
|
|
} |> Async.AwaitTask |> Async.RunSynchronously
|
|
|
|
let _ = builder.Services.AddSingleton<IConnection> conn
|
2022-04-18 01:30:00 +00:00
|
|
|
|
2022-04-22 15:10:45 +00:00
|
|
|
let _ = builder.Services.AddDistributedRethinkDBCache (fun opts ->
|
|
|
|
opts.TableName <- "Session"
|
|
|
|
opts.Connection <- conn)
|
2022-04-19 20:25:51 +00:00
|
|
|
let _ = builder.Services.AddSession(fun opts ->
|
|
|
|
opts.IdleTimeout <- TimeSpan.FromMinutes 30
|
|
|
|
opts.Cookie.HttpOnly <- true
|
|
|
|
opts.Cookie.IsEssential <- true)
|
|
|
|
|
2022-04-21 02:16:29 +00:00
|
|
|
// this needs to be after the session... maybe?
|
|
|
|
let _ = builder.Services.AddGiraffe ()
|
|
|
|
|
2022-04-18 01:30:00 +00:00
|
|
|
// Set up DotLiquid
|
2022-05-22 02:55:13 +00:00
|
|
|
[ typeof<AbsoluteLinkFilter>; typeof<CategoryLinkFilter>; typeof<EditPageLinkFilter>; typeof<EditPostLinkFilter>
|
|
|
|
typeof<NavLinkFilter>; typeof<RelativeLinkFilter>; typeof<TagLinkFilter>; typeof<ValueFilter>
|
2022-05-21 04:07:16 +00:00
|
|
|
]
|
|
|
|
|> List.iter Template.RegisterFilter
|
|
|
|
|
2022-05-22 02:55:13 +00:00
|
|
|
Template.RegisterTag<UserLinksTag> "user_links"
|
2022-04-18 22:06:17 +00:00
|
|
|
|
2022-04-24 03:35:18 +00:00
|
|
|
[ // Domain types
|
2022-05-21 04:07:16 +00:00
|
|
|
typeof<MetaItem>; typeof<Page>; typeof<TagMap>; typeof<WebLog>
|
2022-04-24 03:35:18 +00:00
|
|
|
// View models
|
2022-05-21 04:07:16 +00:00
|
|
|
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayPage>; typeof<EditCategoryModel>
|
|
|
|
typeof<EditPageModel>; typeof<EditPostModel>; typeof<EditTagMapModel>; typeof<EditUserModel>
|
|
|
|
typeof<LogOnModel>; typeof<ManagePermalinksModel>; typeof<PostDisplay>; typeof<PostListItem>
|
|
|
|
typeof<SettingsModel>; typeof<UserMessage>
|
2022-04-24 03:35:18 +00:00
|
|
|
// Framework types
|
2022-04-25 03:21:22 +00:00
|
|
|
typeof<AntiforgeryTokenSet>; typeof<KeyValuePair>; typeof<MetaItem list>; typeof<string list>
|
2022-05-21 04:07:16 +00:00
|
|
|
typeof<string option>; typeof<TagMap list>
|
2022-04-24 03:35:18 +00:00
|
|
|
]
|
|
|
|
|> List.iter (fun it -> Template.RegisterSafeType (it, [| "*" |]))
|
2022-04-17 03:06:38 +00:00
|
|
|
|
|
|
|
let app = builder.Build ()
|
|
|
|
|
|
|
|
match args |> Array.tryHead with
|
2022-04-30 03:46:26 +00:00
|
|
|
| Some it when it = "init" ->
|
|
|
|
NewWebLog.create args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
|
|
|
| Some it when it = "import-permalinks" ->
|
|
|
|
NewWebLog.importPermalinks args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
2022-04-17 03:06:38 +00:00
|
|
|
| _ ->
|
2022-05-22 22:24:09 +00:00
|
|
|
let _ = app.UseForwardedHeaders ()
|
2022-04-17 03:06:38 +00:00
|
|
|
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
|
|
|
let _ = app.UseMiddleware<WebLogMiddleware> ()
|
|
|
|
let _ = app.UseAuthentication ()
|
|
|
|
let _ = app.UseStaticFiles ()
|
|
|
|
let _ = app.UseRouting ()
|
2022-04-19 20:25:51 +00:00
|
|
|
let _ = app.UseSession ()
|
2022-05-22 02:55:13 +00:00
|
|
|
let _ = app.UseGiraffe Handlers.Routes.endpoint
|
2022-04-17 03:06:38 +00:00
|
|
|
|
|
|
|
app.Run()
|
|
|
|
|
|
|
|
0 // Exit code
|
|
|
|
|