Delete user / admin clean-up (#19)
- Add CLI help (#22) - Add constants for common view items - Construct hashes with piped functions
This commit is contained in:
parent
59f385122b
commit
99ccdebcc7
|
@ -255,6 +255,9 @@ type IWebLogUserData =
|
||||||
/// Add a web log user
|
/// Add a web log user
|
||||||
abstract member Add : WebLogUser -> Task<unit>
|
abstract member Add : WebLogUser -> Task<unit>
|
||||||
|
|
||||||
|
/// Delete a web log user
|
||||||
|
abstract member Delete : WebLogUserId -> WebLogId -> Task<Result<bool, string>>
|
||||||
|
|
||||||
/// Find a web log user by their e-mail address
|
/// Find a web log user by their e-mail address
|
||||||
abstract member FindByEmail : email : string -> WebLogId -> Task<WebLogUser option>
|
abstract member FindByEmail : email : string -> WebLogId -> Task<WebLogUser option>
|
||||||
|
|
||||||
|
|
|
@ -955,6 +955,44 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
write; withRetryDefault; ignoreResult conn
|
write; withRetryDefault; ignoreResult conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
member _.FindById userId webLogId =
|
||||||
|
rethink<WebLogUser> {
|
||||||
|
withTable Table.WebLogUser
|
||||||
|
get userId
|
||||||
|
resultOption; withRetryOptionDefault
|
||||||
|
}
|
||||||
|
|> verifyWebLog webLogId (fun u -> u.WebLogId) <| conn
|
||||||
|
|
||||||
|
member this.Delete userId webLogId = backgroundTask {
|
||||||
|
match! this.FindById userId webLogId with
|
||||||
|
| Some _ ->
|
||||||
|
let! pageCount = rethink<int> {
|
||||||
|
withTable Table.Page
|
||||||
|
getAll [ webLogId ] (nameof Page.empty.WebLogId)
|
||||||
|
filter (nameof Page.empty.AuthorId) userId
|
||||||
|
count
|
||||||
|
result; withRetryDefault conn
|
||||||
|
}
|
||||||
|
let! postCount = rethink<int> {
|
||||||
|
withTable Table.Post
|
||||||
|
getAll [ webLogId ] (nameof Post.empty.WebLogId)
|
||||||
|
filter (nameof Post.empty.AuthorId) userId
|
||||||
|
count
|
||||||
|
result; withRetryDefault conn
|
||||||
|
}
|
||||||
|
if pageCount + postCount > 0 then
|
||||||
|
return Result.Error "User has pages or posts; cannot delete"
|
||||||
|
else
|
||||||
|
do! rethink {
|
||||||
|
withTable Table.WebLogUser
|
||||||
|
get userId
|
||||||
|
delete
|
||||||
|
write; withRetryDefault; ignoreResult conn
|
||||||
|
}
|
||||||
|
return Ok true
|
||||||
|
| None -> return Result.Error "User does not exist"
|
||||||
|
}
|
||||||
|
|
||||||
member _.FindByEmail email webLogId =
|
member _.FindByEmail email webLogId =
|
||||||
rethink<WebLogUser list> {
|
rethink<WebLogUser list> {
|
||||||
withTable Table.WebLogUser
|
withTable Table.WebLogUser
|
||||||
|
@ -964,17 +1002,10 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
}
|
}
|
||||||
|> tryFirst <| conn
|
|> tryFirst <| conn
|
||||||
|
|
||||||
member _.FindById userId webLogId =
|
|
||||||
rethink<WebLogUser> {
|
|
||||||
withTable Table.WebLogUser
|
|
||||||
get userId
|
|
||||||
resultOption; withRetryOptionDefault
|
|
||||||
}
|
|
||||||
|> verifyWebLog webLogId (fun u -> u.WebLogId) <| conn
|
|
||||||
|
|
||||||
member _.FindByWebLog webLogId = rethink<WebLogUser list> {
|
member _.FindByWebLog webLogId = rethink<WebLogUser list> {
|
||||||
withTable Table.WebLogUser
|
withTable Table.WebLogUser
|
||||||
getAll [ webLogId ] (nameof WebLogUser.empty.WebLogId)
|
getAll [ webLogId ] (nameof WebLogUser.empty.WebLogId)
|
||||||
|
orderByFunc (fun row -> row[nameof WebLogUser.empty.PreferredName].Downcase ())
|
||||||
result; withRetryDefault conn
|
result; withRetryDefault conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,34 @@ type SQLiteWebLogUserData (conn : SqliteConnection) =
|
||||||
do! write cmd
|
do! write cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find a user by their ID for the given web log
|
||||||
|
let findById userId webLogId = backgroundTask {
|
||||||
|
use cmd = conn.CreateCommand ()
|
||||||
|
cmd.CommandText <- "SELECT * FROM web_log_user WHERE id = @id"
|
||||||
|
cmd.Parameters.AddWithValue ("@id", WebLogUserId.toString userId) |> ignore
|
||||||
|
use! rdr = cmd.ExecuteReaderAsync ()
|
||||||
|
return Helpers.verifyWebLog<WebLogUser> webLogId (fun u -> u.WebLogId) Map.toWebLogUser rdr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a user if they have no posts or pages
|
||||||
|
let delete userId webLogId = backgroundTask {
|
||||||
|
match! findById userId webLogId with
|
||||||
|
| Some _ ->
|
||||||
|
use cmd = conn.CreateCommand ()
|
||||||
|
cmd.CommandText <- "SELECT COUNT(id) FROM page WHERE author_id = @userId"
|
||||||
|
cmd.Parameters.AddWithValue ("@userId", WebLogUserId.toString userId) |> ignore
|
||||||
|
let! pageCount = count cmd
|
||||||
|
cmd.CommandText <- "SELECT COUNT(id) FROM post WHERE author_id = @userId"
|
||||||
|
let! postCount = count cmd
|
||||||
|
if pageCount + postCount > 0 then
|
||||||
|
return Error "User has pages or posts; cannot delete"
|
||||||
|
else
|
||||||
|
cmd.CommandText <- "DELETE FROM web_log_user WHERE id = @userId"
|
||||||
|
let! _ = cmd.ExecuteNonQueryAsync ()
|
||||||
|
return Ok true
|
||||||
|
| None -> return Error "User does not exist"
|
||||||
|
}
|
||||||
|
|
||||||
/// Find a user by their e-mail address for the given web log
|
/// Find a user by their e-mail address for the given web log
|
||||||
let findByEmail (email : string) webLogId = backgroundTask {
|
let findByEmail (email : string) webLogId = backgroundTask {
|
||||||
use cmd = conn.CreateCommand ()
|
use cmd = conn.CreateCommand ()
|
||||||
|
@ -53,19 +81,10 @@ type SQLiteWebLogUserData (conn : SqliteConnection) =
|
||||||
return if rdr.Read () then Some (Map.toWebLogUser rdr) else None
|
return if rdr.Read () then Some (Map.toWebLogUser rdr) else None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a user by their ID for the given web log
|
|
||||||
let findById userId webLogId = backgroundTask {
|
|
||||||
use cmd = conn.CreateCommand ()
|
|
||||||
cmd.CommandText <- "SELECT * FROM web_log_user WHERE id = @id"
|
|
||||||
cmd.Parameters.AddWithValue ("@id", WebLogUserId.toString userId) |> ignore
|
|
||||||
use! rdr = cmd.ExecuteReaderAsync ()
|
|
||||||
return Helpers.verifyWebLog<WebLogUser> webLogId (fun u -> u.WebLogId) Map.toWebLogUser rdr
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all users for the given web log
|
/// Get all users for the given web log
|
||||||
let findByWebLog webLogId = backgroundTask {
|
let findByWebLog webLogId = backgroundTask {
|
||||||
use cmd = conn.CreateCommand ()
|
use cmd = conn.CreateCommand ()
|
||||||
cmd.CommandText <- "SELECT * FROM web_log_user WHERE web_log_id = @webLogId"
|
cmd.CommandText <- "SELECT * FROM web_log_user WHERE web_log_id = @webLogId ORDER BY LOWER(preferred_name)"
|
||||||
addWebLogId cmd webLogId
|
addWebLogId cmd webLogId
|
||||||
use! rdr = cmd.ExecuteReaderAsync ()
|
use! rdr = cmd.ExecuteReaderAsync ()
|
||||||
return toList Map.toWebLogUser rdr
|
return toList Map.toWebLogUser rdr
|
||||||
|
@ -133,6 +152,7 @@ type SQLiteWebLogUserData (conn : SqliteConnection) =
|
||||||
|
|
||||||
interface IWebLogUserData with
|
interface IWebLogUserData with
|
||||||
member _.Add user = add user
|
member _.Add user = add user
|
||||||
|
member _.Delete userId webLogId = delete userId webLogId
|
||||||
member _.FindByEmail email webLogId = findByEmail email webLogId
|
member _.FindByEmail email webLogId = findByEmail email webLogId
|
||||||
member _.FindById userId webLogId = findById userId webLogId
|
member _.FindById userId webLogId = findById userId webLogId
|
||||||
member _.FindByWebLog webLogId = findByWebLog webLogId
|
member _.FindByWebLog webLogId = findByWebLog webLogId
|
||||||
|
|
|
@ -18,17 +18,16 @@ let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
let topCats = getCount data.Category.CountTopLevel
|
let topCats = getCount data.Category.CountTopLevel
|
||||||
let! _ = Task.WhenAll (posts, drafts, pages, listed, cats, topCats)
|
let! _ = Task.WhenAll (posts, drafts, pages, listed, cats, topCats)
|
||||||
return!
|
return!
|
||||||
{| page_title = "Dashboard"
|
hashForPage "Dashboard"
|
||||||
model =
|
|> addToHash ViewContext.Model {
|
||||||
{ Posts = posts.Result
|
Posts = posts.Result
|
||||||
Drafts = drafts.Result
|
Drafts = drafts.Result
|
||||||
Pages = pages.Result
|
Pages = pages.Result
|
||||||
ListedPages = listed.Result
|
ListedPages = listed.Result
|
||||||
Categories = cats.Result
|
Categories = cats.Result
|
||||||
TopLevelCategories = topCats.Result
|
TopLevelCategories = topCats.Result
|
||||||
}
|
}
|
||||||
|}
|
|> adminView "dashboard" next ctx
|
||||||
|> makeHash |> adminView "dashboard" next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- CATEGORIES --
|
// -- CATEGORIES --
|
||||||
|
@ -36,12 +35,10 @@ let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
// GET /admin/categories
|
// GET /admin/categories
|
||||||
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let! catListTemplate = TemplateCache.get "admin" "category-list-body" ctx.Data
|
let! catListTemplate = TemplateCache.get "admin" "category-list-body" ctx.Data
|
||||||
let hash = makeHash {|
|
let! hash =
|
||||||
page_title = "Categories"
|
hashForPage "Categories"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
web_log = ctx.WebLog
|
|> addViewContext ctx
|
||||||
categories = CategoryCache.get ctx
|
|
||||||
|}
|
|
||||||
return!
|
return!
|
||||||
addToHash "category_list" (catListTemplate.Render hash) hash
|
addToHash "category_list" (catListTemplate.Render hash) hash
|
||||||
|> adminView "category-list" next ctx
|
|> adminView "category-list" next ctx
|
||||||
|
@ -49,10 +46,9 @@ let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
||||||
|
|
||||||
// GET /admin/categories/bare
|
// GET /admin/categories/bare
|
||||||
let listCategoriesBare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
let listCategoriesBare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
||||||
{| categories = CategoryCache.get ctx
|
hashForPage "Categories"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
|}
|
|> adminBareView "category-list-body" next ctx
|
||||||
|> makeHash |> adminBareView "category-list-body" next ctx
|
|
||||||
|
|
||||||
|
|
||||||
// GET /admin/category/{id}/edit
|
// GET /admin/category/{id}/edit
|
||||||
|
@ -67,13 +63,11 @@ let editCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ct
|
||||||
}
|
}
|
||||||
match result with
|
match result with
|
||||||
| Some (title, cat) ->
|
| Some (title, cat) ->
|
||||||
return! {|
|
return!
|
||||||
page_title = title
|
hashForPage title
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = EditCategoryModel.fromCategory cat
|
|> addToHash ViewContext.Model (EditCategoryModel.fromCategory cat)
|
||||||
categories = CategoryCache.get ctx
|
|> adminBareView "category-edit" next ctx
|
||||||
|}
|
|
||||||
|> makeHash |> adminBareView "category-edit" next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,12 +111,12 @@ open Microsoft.AspNetCore.Http
|
||||||
/// Get the hash necessary to render the tag mapping list
|
/// Get the hash necessary to render the tag mapping list
|
||||||
let private tagMappingHash (ctx : HttpContext) = task {
|
let private tagMappingHash (ctx : HttpContext) = task {
|
||||||
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
|
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
|
||||||
return makeHash {|
|
return!
|
||||||
csrf = ctx.CsrfTokenSet
|
hashForPage "Tag Mappings"
|
||||||
web_log = ctx.WebLog
|
|> withAntiCsrf ctx
|
||||||
mappings = mappings
|
|> addToHash "mappings" mappings
|
||||||
mapping_ids = mappings |> List.map (fun it -> { Name = it.Tag; Value = TagMapId.toString it.Id })
|
|> addToHash "mapping_ids" (mappings |> List.map (fun it -> { Name = it.Tag; Value = TagMapId.toString it.Id }))
|
||||||
|}
|
|> addViewContext ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/settings/tag-mappings
|
// GET /admin/settings/tag-mappings
|
||||||
|
@ -131,7 +125,6 @@ let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> ta
|
||||||
let! listTemplate = TemplateCache.get "admin" "tag-mapping-list-body" ctx.Data
|
let! listTemplate = TemplateCache.get "admin" "tag-mapping-list-body" ctx.Data
|
||||||
return!
|
return!
|
||||||
addToHash "tag_mapping_list" (listTemplate.Render hash) hash
|
addToHash "tag_mapping_list" (listTemplate.Render hash) hash
|
||||||
|> addToHash "page_title" "Tag Mappings"
|
|
||||||
|> adminView "tag-mapping-list" next ctx
|
|> adminView "tag-mapping-list" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,12 +142,11 @@ let editMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next
|
||||||
else ctx.Data.TagMap.FindById (TagMapId tagMapId) ctx.WebLog.Id
|
else ctx.Data.TagMap.FindById (TagMapId tagMapId) ctx.WebLog.Id
|
||||||
match! tagMap with
|
match! tagMap with
|
||||||
| Some tm ->
|
| Some tm ->
|
||||||
return! {|
|
return!
|
||||||
page_title = if isNew then "Add Tag Mapping" else $"Mapping for {tm.Tag} Tag"
|
hashForPage (if isNew then "Add Tag Mapping" else $"Mapping for {tm.Tag} Tag")
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = EditTagMapModel.fromMapping tm
|
|> addToHash ViewContext.Model (EditTagMapModel.fromMapping tm)
|
||||||
|}
|
|> adminBareView "tag-mapping-edit" next ctx
|
||||||
|> makeHash |> adminBareView "tag-mapping-edit" next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,10 +183,9 @@ open MyWebLog.Data
|
||||||
|
|
||||||
// GET /admin/theme/update
|
// GET /admin/theme/update
|
||||||
let themeUpdatePage : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
let themeUpdatePage : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
||||||
{| page_title = "Upload Theme"
|
hashForPage "Upload Theme"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
|}
|
|> adminView "upload-theme" next ctx
|
||||||
|> makeHash |> adminView "upload-theme" next ctx
|
|
||||||
|
|
||||||
/// Update the name and version for a theme based on the version.txt file, if present
|
/// Update the name and version for a theme based on the version.txt file, if present
|
||||||
let private updateNameAndVersion (theme : Theme) (zip : ZipArchive) = backgroundTask {
|
let private updateNameAndVersion (theme : Theme) (zip : ZipArchive) = backgroundTask {
|
||||||
|
@ -244,9 +235,9 @@ let private updateAssets themeId (zip : ZipArchive) (data : IData) = backgroundT
|
||||||
use stream = new MemoryStream ()
|
use stream = new MemoryStream ()
|
||||||
do! asset.Open().CopyToAsync stream
|
do! asset.Open().CopyToAsync stream
|
||||||
do! data.ThemeAsset.Save
|
do! data.ThemeAsset.Save
|
||||||
{ Id = ThemeAssetId (themeId, assetName)
|
{ Id = ThemeAssetId (themeId, assetName)
|
||||||
UpdatedOn = asset.LastWriteTime.DateTime
|
UpdatedOn = asset.LastWriteTime.DateTime
|
||||||
Data = stream.ToArray ()
|
Data = stream.ToArray ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,28 +294,28 @@ let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
let! allPages = data.Page.All ctx.WebLog.Id
|
let! allPages = data.Page.All ctx.WebLog.Id
|
||||||
let! themes = data.Theme.All ()
|
let! themes = data.Theme.All ()
|
||||||
return! {|
|
return!
|
||||||
page_title = "Web Log Settings"
|
hashForPage "Web Log Settings"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = SettingsModel.fromWebLog ctx.WebLog
|
|> addToHash ViewContext.Model (SettingsModel.fromWebLog ctx.WebLog)
|
||||||
pages = seq
|
|> addToHash "pages" (
|
||||||
{ KeyValuePair.Create ("posts", "- First Page of Posts -")
|
seq {
|
||||||
|
KeyValuePair.Create ("posts", "- First Page of Posts -")
|
||||||
yield! allPages
|
yield! allPages
|
||||||
|> List.sortBy (fun p -> p.Title.ToLower ())
|
|> List.sortBy (fun p -> p.Title.ToLower ())
|
||||||
|> List.map (fun p -> KeyValuePair.Create (PageId.toString p.Id, p.Title))
|
|> List.map (fun p -> KeyValuePair.Create (PageId.toString p.Id, p.Title))
|
||||||
}
|
}
|
||||||
|> Array.ofSeq
|
|> Array.ofSeq)
|
||||||
themes =
|
|> addToHash "themes" (
|
||||||
themes
|
themes
|
||||||
|> Seq.ofList
|
|> Seq.ofList
|
||||||
|> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
|
|> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
|
||||||
|> Array.ofSeq
|
|> Array.ofSeq)
|
||||||
upload_values = [|
|
|> addToHash "upload_values" [|
|
||||||
KeyValuePair.Create (UploadDestination.toString Database, "Database")
|
KeyValuePair.Create (UploadDestination.toString Database, "Database")
|
||||||
KeyValuePair.Create (UploadDestination.toString Disk, "Disk")
|
KeyValuePair.Create (UploadDestination.toString Disk, "Disk")
|
||||||
|]
|
|]
|
||||||
|}
|
|> adminView "settings" next ctx
|
||||||
|> makeHash |> adminView "settings" next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/settings
|
// POST /admin/settings
|
||||||
|
|
|
@ -416,16 +416,14 @@ let generate (feedType : FeedType) postCount : HttpHandler = fun next ctx -> bac
|
||||||
|
|
||||||
// GET /admin/settings/rss
|
// GET /admin/settings/rss
|
||||||
let editSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
let editSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
||||||
let feeds =
|
hashForPage "RSS Settings"
|
||||||
|
|> withAntiCsrf ctx
|
||||||
|
|> addToHash ViewContext.Model (EditRssModel.fromRssOptions ctx.WebLog.Rss)
|
||||||
|
|> addToHash "custom_feeds" (
|
||||||
ctx.WebLog.Rss.CustomFeeds
|
ctx.WebLog.Rss.CustomFeeds
|
||||||
|> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx))
|
|> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx))
|
||||||
|> Array.ofList
|
|> Array.ofList)
|
||||||
{| page_title = "RSS Settings"
|
|> adminView "rss-settings" next ctx
|
||||||
csrf = ctx.CsrfTokenSet
|
|
||||||
model = EditRssModel.fromRssOptions ctx.WebLog.Rss
|
|
||||||
custom_feeds = feeds
|
|
||||||
|}
|
|
||||||
|> makeHash |> adminView "rss-settings" next ctx
|
|
||||||
|
|
||||||
// POST /admin/settings/rss
|
// POST /admin/settings/rss
|
||||||
let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
|
@ -449,22 +447,20 @@ let editCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next
|
||||||
| _ -> ctx.WebLog.Rss.CustomFeeds |> List.tryFind (fun f -> f.Id = CustomFeedId feedId)
|
| _ -> ctx.WebLog.Rss.CustomFeeds |> List.tryFind (fun f -> f.Id = CustomFeedId feedId)
|
||||||
match customFeed with
|
match customFeed with
|
||||||
| Some f ->
|
| Some f ->
|
||||||
{| page_title = $"""{if feedId = "new" then "Add" else "Edit"} Custom RSS Feed"""
|
hashForPage $"""{if feedId = "new" then "Add" else "Edit"} Custom RSS Feed"""
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = EditCustomFeedModel.fromFeed f
|
|> addToHash ViewContext.Model (EditCustomFeedModel.fromFeed f)
|
||||||
categories = CategoryCache.get ctx
|
|> addToHash "medium_values" [|
|
||||||
medium_values = [|
|
KeyValuePair.Create ("", "– Unspecified –")
|
||||||
KeyValuePair.Create ("", "– Unspecified –")
|
KeyValuePair.Create (PodcastMedium.toString Podcast, "Podcast")
|
||||||
KeyValuePair.Create (PodcastMedium.toString Podcast, "Podcast")
|
KeyValuePair.Create (PodcastMedium.toString Music, "Music")
|
||||||
KeyValuePair.Create (PodcastMedium.toString Music, "Music")
|
KeyValuePair.Create (PodcastMedium.toString Video, "Video")
|
||||||
KeyValuePair.Create (PodcastMedium.toString Video, "Video")
|
KeyValuePair.Create (PodcastMedium.toString Film, "Film")
|
||||||
KeyValuePair.Create (PodcastMedium.toString Film, "Film")
|
KeyValuePair.Create (PodcastMedium.toString Audiobook, "Audiobook")
|
||||||
KeyValuePair.Create (PodcastMedium.toString Audiobook, "Audiobook")
|
KeyValuePair.Create (PodcastMedium.toString Newsletter, "Newsletter")
|
||||||
KeyValuePair.Create (PodcastMedium.toString Newsletter, "Newsletter")
|
KeyValuePair.Create (PodcastMedium.toString Blog, "Blog")
|
||||||
KeyValuePair.Create (PodcastMedium.toString Blog, "Blog")
|
|]
|
||||||
|]
|
|> adminView "custom-feed-edit" next ctx
|
||||||
|}
|
|
||||||
|> makeHash |> adminView "custom-feed-edit" next ctx
|
|
||||||
| None -> Error.notFound next ctx
|
| None -> Error.notFound next ctx
|
||||||
|
|
||||||
// POST /admin/settings/rss/save
|
// POST /admin/settings/rss/save
|
||||||
|
|
|
@ -12,12 +12,117 @@ type ISession with
|
||||||
this.SetString (key, JsonSerializer.Serialize item)
|
this.SetString (key, JsonSerializer.Serialize item)
|
||||||
|
|
||||||
/// Get an item from the session
|
/// Get an item from the session
|
||||||
member this.Get<'T> key =
|
member this.TryGet<'T> key =
|
||||||
match this.GetString key with
|
match this.GetString key with
|
||||||
| null -> None
|
| null -> None
|
||||||
| item -> Some (JsonSerializer.Deserialize<'T> item)
|
| item -> Some (JsonSerializer.Deserialize<'T> item)
|
||||||
|
|
||||||
|
|
||||||
|
/// Keys used in the myWebLog-standard DotLiquid hash
|
||||||
|
module ViewContext =
|
||||||
|
|
||||||
|
/// The anti cross-site request forgery (CSRF) token set to use for form submissions
|
||||||
|
[<Literal>]
|
||||||
|
let AntiCsrfTokens = "csrf"
|
||||||
|
|
||||||
|
/// The categories for this web log
|
||||||
|
[<Literal>]
|
||||||
|
let Categories = "categories"
|
||||||
|
|
||||||
|
/// The main content of the view
|
||||||
|
[<Literal>]
|
||||||
|
let Content = "content"
|
||||||
|
|
||||||
|
/// The current page URL
|
||||||
|
[<Literal>]
|
||||||
|
let CurrentPage = "current_page"
|
||||||
|
|
||||||
|
/// The generator string for the current version of myWebLog
|
||||||
|
[<Literal>]
|
||||||
|
let Generator = "generator"
|
||||||
|
|
||||||
|
/// The HTML to load htmx from the unpkg CDN
|
||||||
|
[<Literal>]
|
||||||
|
let HtmxScript = "htmx_script"
|
||||||
|
|
||||||
|
/// Whether the current user has Administrator privileges
|
||||||
|
[<Literal>]
|
||||||
|
let IsAdministrator = "is_administrator"
|
||||||
|
|
||||||
|
/// Whether the current user has Author (or above) privileges
|
||||||
|
[<Literal>]
|
||||||
|
let IsAuthor = "is_author"
|
||||||
|
|
||||||
|
/// Whether the current view is displaying a category archive page
|
||||||
|
[<Literal>]
|
||||||
|
let IsCategory = "is_category"
|
||||||
|
|
||||||
|
/// Whether the current view is displaying the first page of a category archive
|
||||||
|
[<Literal>]
|
||||||
|
let IsCategoryHome = "is_category_home"
|
||||||
|
|
||||||
|
/// Whether the current user has Editor (or above) privileges
|
||||||
|
[<Literal>]
|
||||||
|
let IsEditor = "is_editor"
|
||||||
|
|
||||||
|
/// Whether the current view is the home page for the web log
|
||||||
|
[<Literal>]
|
||||||
|
let IsHome = "is_home"
|
||||||
|
|
||||||
|
/// Whether there is a user logged on
|
||||||
|
[<Literal>]
|
||||||
|
let IsLoggedOn = "is_logged_on"
|
||||||
|
|
||||||
|
/// Whether the current view is displaying a page
|
||||||
|
[<Literal>]
|
||||||
|
let IsPage = "is_page"
|
||||||
|
|
||||||
|
/// Whether the current view is displaying a post
|
||||||
|
[<Literal>]
|
||||||
|
let IsPost = "is_post"
|
||||||
|
|
||||||
|
/// Whether the current view is a tag archive page
|
||||||
|
[<Literal>]
|
||||||
|
let IsTag = "is_tag"
|
||||||
|
|
||||||
|
/// Whether the current view is the first page of a tag archive
|
||||||
|
[<Literal>]
|
||||||
|
let IsTagHome = "is_tag_home"
|
||||||
|
|
||||||
|
/// Whether the current user has Web Log Admin (or above) privileges
|
||||||
|
[<Literal>]
|
||||||
|
let IsWebLogAdmin = "is_web_log_admin"
|
||||||
|
|
||||||
|
/// Messages to be displayed to the user
|
||||||
|
[<Literal>]
|
||||||
|
let Messages = "messages"
|
||||||
|
|
||||||
|
/// The view model / form for the page
|
||||||
|
[<Literal>]
|
||||||
|
let Model = "model"
|
||||||
|
|
||||||
|
/// The listed pages for the web log
|
||||||
|
[<Literal>]
|
||||||
|
let PageList = "page_list"
|
||||||
|
|
||||||
|
/// The title of the page being displayed
|
||||||
|
[<Literal>]
|
||||||
|
let PageTitle = "page_title"
|
||||||
|
|
||||||
|
/// The slug for category or tag archive pages
|
||||||
|
[<Literal>]
|
||||||
|
let Slug = "slug"
|
||||||
|
|
||||||
|
/// The ID of the current user
|
||||||
|
[<Literal>]
|
||||||
|
let UserId = "user_id"
|
||||||
|
|
||||||
|
/// The current web log
|
||||||
|
[<Literal>]
|
||||||
|
let WebLog = "web_log"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// The HTTP item key for loading the session
|
/// The HTTP item key for loading the session
|
||||||
let private sessionLoadedKey = "session-loaded"
|
let private sessionLoadedKey = "session-loaded"
|
||||||
|
|
||||||
|
@ -38,34 +143,41 @@ open MyWebLog.ViewModels
|
||||||
/// Add a message to the user's session
|
/// Add a message to the user's session
|
||||||
let addMessage (ctx : HttpContext) message = task {
|
let addMessage (ctx : HttpContext) message = task {
|
||||||
do! loadSession ctx
|
do! loadSession ctx
|
||||||
let msg = match ctx.Session.Get<UserMessage list> "messages" with Some it -> it | None -> []
|
let msg = match ctx.Session.TryGet<UserMessage list> ViewContext.Messages with Some it -> it | None -> []
|
||||||
ctx.Session.Set ("messages", message :: msg)
|
ctx.Session.Set (ViewContext.Messages, message :: msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get any messages from the user's session, removing them in the process
|
/// Get any messages from the user's session, removing them in the process
|
||||||
let messages (ctx : HttpContext) = task {
|
let messages (ctx : HttpContext) = task {
|
||||||
do! loadSession ctx
|
do! loadSession ctx
|
||||||
match ctx.Session.Get<UserMessage list> "messages" with
|
match ctx.Session.TryGet<UserMessage list> ViewContext.Messages with
|
||||||
| Some msg ->
|
| Some msg ->
|
||||||
ctx.Session.Remove "messages"
|
ctx.Session.Remove ViewContext.Messages
|
||||||
return msg |> (List.rev >> Array.ofList)
|
return msg |> (List.rev >> Array.ofList)
|
||||||
| None -> return [||]
|
| None -> return [||]
|
||||||
}
|
}
|
||||||
|
|
||||||
open System.Collections.Generic
|
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
open DotLiquid
|
open DotLiquid
|
||||||
|
|
||||||
|
/// Shorthand for creating a DotLiquid hash from an anonymous object
|
||||||
let makeHash (values : obj) =
|
let makeHash (values : obj) =
|
||||||
Hash.FromAnonymousObject values
|
Hash.FromAnonymousObject values
|
||||||
|
|
||||||
|
/// Create a hash with the page title filled
|
||||||
|
let hashForPage (title : string) =
|
||||||
|
makeHash {| page_title = title |}
|
||||||
|
|
||||||
/// Add a key to the hash, returning the modified hash
|
/// Add a key to the hash, returning the modified hash
|
||||||
// (note that the hash itself is mutated; this is only used to make it pipeable)
|
// (note that the hash itself is mutated; this is only used to make it pipeable)
|
||||||
let addToHash key (value : obj) (hash : Hash) =
|
let addToHash key (value : obj) (hash : Hash) =
|
||||||
if hash.ContainsKey key then hash[key] <- value else hash.Add (key, value)
|
if hash.ContainsKey key then hash[key] <- value else hash.Add (key, value)
|
||||||
hash
|
hash
|
||||||
|
|
||||||
|
/// Add anti-CSRF tokens to the given hash
|
||||||
|
let withAntiCsrf (ctx : HttpContext) =
|
||||||
|
addToHash ViewContext.AntiCsrfTokens ctx.CsrfTokenSet
|
||||||
|
|
||||||
open System.Security.Claims
|
open System.Security.Claims
|
||||||
open Giraffe
|
open Giraffe
|
||||||
open Giraffe.Htmx
|
open Giraffe.Htmx
|
||||||
|
@ -75,27 +187,31 @@ open Giraffe.ViewEngine
|
||||||
let private htmxScript = RenderView.AsString.htmlNode Htmx.Script.minified
|
let private htmxScript = RenderView.AsString.htmlNode Htmx.Script.minified
|
||||||
|
|
||||||
/// Populate the DotLiquid hash with standard information
|
/// Populate the DotLiquid hash with standard information
|
||||||
let private populateHash hash ctx = task {
|
let addViewContext ctx (hash : Hash) = task {
|
||||||
let! messages = messages ctx
|
let! messages = messages ctx
|
||||||
do! commitSession ctx
|
do! commitSession ctx
|
||||||
|
|
||||||
ctx.User.Claims
|
|
||||||
|> Seq.tryFind (fun claim -> claim.Type = ClaimTypes.NameIdentifier)
|
|
||||||
|> Option.map (fun claim -> claim.Value)
|
|
||||||
|> Option.iter (fun userId -> addToHash "user_id" userId hash |> ignore)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
addToHash "web_log" ctx.WebLog hash
|
if hash.ContainsKey ViewContext.HtmxScript && hash.ContainsKey ViewContext.Messages then
|
||||||
|> addToHash "page_list" (PageListCache.get ctx)
|
// We have already populated everything; just update messages
|
||||||
|> addToHash "current_page" ctx.Request.Path.Value[1..]
|
hash[ViewContext.Messages] <- Array.concat [ hash[ViewContext.Messages] :?> UserMessage[]; messages ]
|
||||||
|> addToHash "messages" messages
|
hash
|
||||||
|> addToHash "generator" ctx.Generator
|
else
|
||||||
|> addToHash "htmx_script" htmxScript
|
ctx.User.Claims
|
||||||
|> addToHash "is_logged_on" ctx.User.Identity.IsAuthenticated
|
|> Seq.tryFind (fun claim -> claim.Type = ClaimTypes.NameIdentifier)
|
||||||
|> addToHash "is_author" (ctx.HasAccessLevel Author)
|
|> Option.map (fun claim -> addToHash ViewContext.UserId claim.Value hash)
|
||||||
|> addToHash "is_editor" (ctx.HasAccessLevel Editor)
|
|> Option.defaultValue hash
|
||||||
|> addToHash "is_web_log_admin" (ctx.HasAccessLevel WebLogAdmin)
|
|> addToHash ViewContext.WebLog ctx.WebLog
|
||||||
|> addToHash "is_administrator" (ctx.HasAccessLevel Administrator)
|
|> addToHash ViewContext.PageList (PageListCache.get ctx)
|
||||||
|
|> addToHash ViewContext.Categories (CategoryCache.get ctx)
|
||||||
|
|> addToHash ViewContext.CurrentPage ctx.Request.Path.Value[1..]
|
||||||
|
|> addToHash ViewContext.Messages messages
|
||||||
|
|> addToHash ViewContext.Generator ctx.Generator
|
||||||
|
|> addToHash ViewContext.HtmxScript htmxScript
|
||||||
|
|> addToHash ViewContext.IsLoggedOn ctx.User.Identity.IsAuthenticated
|
||||||
|
|> addToHash ViewContext.IsAuthor (ctx.HasAccessLevel Author)
|
||||||
|
|> addToHash ViewContext.IsEditor (ctx.HasAccessLevel Editor)
|
||||||
|
|> addToHash ViewContext.IsWebLogAdmin (ctx.HasAccessLevel WebLogAdmin)
|
||||||
|
|> addToHash ViewContext.IsAdministrator (ctx.HasAccessLevel Administrator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is the request from htmx?
|
/// Is the request from htmx?
|
||||||
|
@ -104,16 +220,14 @@ let isHtmx (ctx : HttpContext) =
|
||||||
|
|
||||||
/// Render a view for the specified theme, using the specified template, layout, and hash
|
/// Render a view for the specified theme, using the specified template, layout, and hash
|
||||||
let viewForTheme themeId template next ctx (hash : Hash) = task {
|
let viewForTheme themeId template next ctx (hash : Hash) = task {
|
||||||
if not (hash.ContainsKey "htmx_script") then
|
let! hash = addViewContext ctx hash
|
||||||
let! _ = populateHash hash ctx
|
|
||||||
()
|
|
||||||
let (ThemeId theme) = themeId
|
let (ThemeId theme) = themeId
|
||||||
// NOTE: DotLiquid does not support {% render %} or {% include %} in its templates, so we will do a 2-pass render;
|
// NOTE: DotLiquid does not support {% render %} or {% include %} in its templates, so we will do a 2-pass render;
|
||||||
// the net effect is a "layout" capability similar to Razor or Pug
|
// the net effect is a "layout" capability similar to Razor or Pug
|
||||||
|
|
||||||
// Render view content...
|
// Render view content...
|
||||||
let! contentTemplate = TemplateCache.get theme template ctx.Data
|
let! contentTemplate = TemplateCache.get theme template ctx.Data
|
||||||
let _ = addToHash "content" (contentTemplate.Render hash) hash
|
let _ = addToHash ViewContext.Content (contentTemplate.Render hash) hash
|
||||||
|
|
||||||
// ...then render that content with its layout
|
// ...then render that content with its layout
|
||||||
let! layoutTemplate = TemplateCache.get theme (if isHtmx ctx then "layout-partial" else "layout") ctx.Data
|
let! layoutTemplate = TemplateCache.get theme (if isHtmx ctx then "layout-partial" else "layout") ctx.Data
|
||||||
|
@ -137,24 +251,25 @@ let messagesToHeaders (messages : UserMessage array) : HttpHandler =
|
||||||
|
|
||||||
/// Render a bare view for the specified theme, using the specified template and hash
|
/// Render a bare view for the specified theme, using the specified template and hash
|
||||||
let bareForTheme themeId template next ctx (hash : Hash) = task {
|
let bareForTheme themeId template next ctx (hash : Hash) = task {
|
||||||
let! hash = populateHash hash ctx
|
let! hash = addViewContext ctx hash
|
||||||
let (ThemeId theme) = themeId
|
let (ThemeId theme) = themeId
|
||||||
|
|
||||||
if not (hash.ContainsKey "content") then
|
if not (hash.ContainsKey ViewContext.Content) then
|
||||||
let! contentTemplate = TemplateCache.get theme template ctx.Data
|
let! contentTemplate = TemplateCache.get theme template ctx.Data
|
||||||
addToHash "content" (contentTemplate.Render hash) hash |> ignore
|
addToHash ViewContext.Content (contentTemplate.Render hash) hash |> ignore
|
||||||
|
|
||||||
// Bare templates are rendered with layout-bare
|
// Bare templates are rendered with layout-bare
|
||||||
let! layoutTemplate = TemplateCache.get theme "layout-bare" ctx.Data
|
let! layoutTemplate = TemplateCache.get theme "layout-bare" ctx.Data
|
||||||
|
|
||||||
return!
|
return!
|
||||||
(messagesToHeaders (hash["messages"] :?> UserMessage[]) >=> htmlString (layoutTemplate.Render hash)) next ctx
|
(messagesToHeaders (hash[ViewContext.Messages] :?> UserMessage[])
|
||||||
|
>=> htmlString (layoutTemplate.Render hash))
|
||||||
|
next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a view for the web log's default theme
|
/// Return a view for the web log's default theme
|
||||||
let themedView template next ctx hash = task {
|
let themedView template next ctx hash = task {
|
||||||
let! hash = populateHash hash ctx
|
let! hash = addViewContext ctx hash
|
||||||
return! viewForTheme (hash["web_log"] :?> WebLog).ThemeId template next ctx hash
|
return! viewForTheme (hash[ViewContext.WebLog] :?> WebLog).ThemeId template next ctx hash
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Display a view for the admin theme
|
/// Display a view for the admin theme
|
||||||
|
@ -171,7 +286,7 @@ let redirectToGet url : HttpHandler = fun _ ctx -> task {
|
||||||
return! redirectTo false (WebLog.relativeUrl ctx.WebLog (Permalink url)) earlyReturn ctx
|
return! redirectTo false (WebLog.relativeUrl ctx.WebLog (Permalink url)) earlyReturn ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate the cross-site request forgery token in the current request
|
/// Validate the anti cross-site request forgery token in the current request
|
||||||
let validateCsrf : HttpHandler = fun next ctx -> task {
|
let validateCsrf : HttpHandler = fun next ctx -> task {
|
||||||
match! ctx.AntiForgery.IsRequestValidAsync ctx with
|
match! ctx.AntiForgery.IsRequestValidAsync ctx with
|
||||||
| true -> return! next ctx
|
| true -> return! next ctx
|
||||||
|
|
|
@ -9,15 +9,14 @@ open MyWebLog.ViewModels
|
||||||
// GET /admin/pages/page/{pageNbr}
|
// GET /admin/pages/page/{pageNbr}
|
||||||
let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
let! pages = ctx.Data.Page.FindPageOfPages ctx.WebLog.Id pageNbr
|
let! pages = ctx.Data.Page.FindPageOfPages ctx.WebLog.Id pageNbr
|
||||||
return! {|
|
return!
|
||||||
page_title = "Pages"
|
hashForPage "Pages"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
pages = pages |> List.map (DisplayPage.fromPageMinimal ctx.WebLog)
|
|> addToHash "pages" (pages |> List.map (DisplayPage.fromPageMinimal ctx.WebLog))
|
||||||
page_nbr = pageNbr
|
|> addToHash "page_nbr" pageNbr
|
||||||
prev_page = if pageNbr = 2 then "" else $"/page/{pageNbr - 1}"
|
|> addToHash "prev_page" (if pageNbr = 2 then "" else $"/page/{pageNbr - 1}")
|
||||||
next_page = $"/page/{pageNbr + 1}"
|
|> addToHash "next_page" $"/page/{pageNbr + 1}"
|
||||||
|}
|
|> adminView "page-list" next ctx
|
||||||
|> makeHash |> adminView "page-list" next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/page/{id}/edit
|
// GET /admin/page/{id}/edit
|
||||||
|
@ -34,15 +33,15 @@ let edit pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
| Some (title, page) when canEdit page.AuthorId ctx ->
|
| Some (title, page) when canEdit page.AuthorId ctx ->
|
||||||
let model = EditPageModel.fromPage page
|
let model = EditPageModel.fromPage page
|
||||||
let! templates = templatesForTheme ctx "page"
|
let! templates = templatesForTheme ctx "page"
|
||||||
return! {|
|
return!
|
||||||
page_title = title
|
hashForPage title
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = model
|
|> addToHash ViewContext.Model model
|
||||||
metadata = Array.zip model.MetaNames model.MetaValues
|
|> addToHash "metadata" (
|
||||||
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
|
Array.zip model.MetaNames model.MetaValues
|
||||||
templates = templates
|
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |]))
|
||||||
|}
|
|> addToHash "templates" templates
|
||||||
|> makeHash |> adminView "page-edit" next ctx
|
|> adminView "page-edit" next ctx
|
||||||
| Some _ -> return! Error.notAuthorized next ctx
|
| Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
@ -61,12 +60,11 @@ let delete pgId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> ta
|
||||||
let editPermalinks pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let editPermalinks pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
|
match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
|
||||||
| Some pg when canEdit pg.AuthorId ctx ->
|
| Some pg when canEdit pg.AuthorId ctx ->
|
||||||
return! {|
|
return!
|
||||||
page_title = "Manage Prior Permalinks"
|
hashForPage "Manage Prior Permalinks"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = ManagePermalinksModel.fromPage pg
|
|> addToHash ViewContext.Model (ManagePermalinksModel.fromPage pg)
|
||||||
|}
|
|> adminView "permalinks" next ctx
|
||||||
|> makeHash |> adminView "permalinks" next ctx
|
|
||||||
| Some _ -> return! Error.notAuthorized next ctx
|
| Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
@ -91,12 +89,11 @@ let savePermalinks : HttpHandler = requireAccess Author >=> fun next ctx -> task
|
||||||
let editRevisions pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let editRevisions pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
|
match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
|
||||||
| Some pg when canEdit pg.AuthorId ctx ->
|
| Some pg when canEdit pg.AuthorId ctx ->
|
||||||
return! {|
|
return!
|
||||||
page_title = "Manage Page Revisions"
|
hashForPage "Manage Page Revisions"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = ManageRevisionsModel.fromPage ctx.WebLog pg
|
|> addToHash ViewContext.Model (ManageRevisionsModel.fromPage ctx.WebLog pg)
|
||||||
|}
|
|> adminView "revisions" next ctx
|
||||||
|> makeHash |> adminView "revisions" next ctx
|
|
||||||
| Some _ -> return! Error.notAuthorized next ctx
|
| Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ open MyWebLog.Data
|
||||||
open MyWebLog.ViewModels
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
/// Convert a list of posts into items ready to be displayed
|
/// Convert a list of posts into items ready to be displayed
|
||||||
let preparePostList webLog posts listType (url : string) pageNbr perPage ctx (data : IData) = task {
|
let preparePostList webLog posts listType (url : string) pageNbr perPage (data : IData) = task {
|
||||||
let! authors = getAuthors webLog posts data
|
let! authors = getAuthors webLog posts data
|
||||||
let! tagMappings = getTagMappings webLog posts data
|
let! tagMappings = getTagMappings webLog posts data
|
||||||
let relUrl it = Some <| WebLog.relativeUrl webLog (Permalink it)
|
let relUrl it = Some <| WebLog.relativeUrl webLog (Permalink it)
|
||||||
|
@ -85,12 +85,11 @@ let preparePostList webLog posts listType (url : string) pageNbr perPage ctx (da
|
||||||
OlderLink = olderLink
|
OlderLink = olderLink
|
||||||
OlderName = olderPost |> Option.map (fun p -> p.Title)
|
OlderName = olderPost |> Option.map (fun p -> p.Title)
|
||||||
}
|
}
|
||||||
return makeHash {|
|
return
|
||||||
model = model
|
makeHash {||}
|
||||||
categories = CategoryCache.get ctx
|
|> addToHash ViewContext.Model model
|
||||||
tag_mappings = tagMappings
|
|> addToHash "tag_mappings" tagMappings
|
||||||
is_post = match listType with SinglePost -> true | _ -> false
|
|> addToHash ViewContext.IsPost (match listType with SinglePost -> true | _ -> false)
|
||||||
|}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open Giraffe
|
open Giraffe
|
||||||
|
@ -100,15 +99,18 @@ let pageOfPosts pageNbr : HttpHandler = fun next ctx -> task {
|
||||||
let count = ctx.WebLog.PostsPerPage
|
let count = ctx.WebLog.PostsPerPage
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
let! posts = data.Post.FindPageOfPublishedPosts ctx.WebLog.Id pageNbr count
|
let! posts = data.Post.FindPageOfPublishedPosts ctx.WebLog.Id pageNbr count
|
||||||
let! hash = preparePostList ctx.WebLog posts PostList "" pageNbr count ctx data
|
let! hash = preparePostList ctx.WebLog posts PostList "" pageNbr count data
|
||||||
let title =
|
let title =
|
||||||
match pageNbr, ctx.WebLog.DefaultPage with
|
match pageNbr, ctx.WebLog.DefaultPage with
|
||||||
| 1, "posts" -> None
|
| 1, "posts" -> None
|
||||||
| _, "posts" -> Some $"Page {pageNbr}"
|
| _, "posts" -> Some $"Page {pageNbr}"
|
||||||
| _, _ -> Some $"Page {pageNbr} « Posts"
|
| _, _ -> Some $"Page {pageNbr} « Posts"
|
||||||
match title with Some ttl -> hash.Add ("page_title", ttl) | None -> ()
|
return!
|
||||||
if pageNbr = 1 && ctx.WebLog.DefaultPage = "posts" then hash.Add ("is_home", true)
|
match title with Some ttl -> addToHash ViewContext.PageTitle ttl hash | None -> hash
|
||||||
return! themedView "index" next ctx hash
|
|> function
|
||||||
|
| hash ->
|
||||||
|
if pageNbr = 1 && ctx.WebLog.DefaultPage = "posts" then addToHash ViewContext.IsHome true hash else hash
|
||||||
|
|> themedView "index" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /page/{pageNbr}/
|
// GET /page/{pageNbr}/
|
||||||
|
@ -131,14 +133,14 @@ let pageOfCategorizedPosts slugAndPage : HttpHandler = fun next ctx -> task {
|
||||||
match! data.Post.FindPageOfCategorizedPosts webLog.Id (getCategoryIds slug ctx) pageNbr webLog.PostsPerPage
|
match! data.Post.FindPageOfCategorizedPosts webLog.Id (getCategoryIds slug ctx) pageNbr webLog.PostsPerPage
|
||||||
with
|
with
|
||||||
| posts when List.length posts > 0 ->
|
| posts when List.length posts > 0 ->
|
||||||
let! hash = preparePostList webLog posts CategoryList cat.Slug pageNbr webLog.PostsPerPage ctx data
|
let! hash = preparePostList webLog posts CategoryList cat.Slug pageNbr webLog.PostsPerPage data
|
||||||
let pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>"""
|
let pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>"""
|
||||||
return!
|
return!
|
||||||
addToHash "page_title" $"{cat.Name}: Category Archive{pgTitle}" hash
|
addToHash ViewContext.PageTitle $"{cat.Name}: Category Archive{pgTitle}" hash
|
||||||
|> addToHash "subtitle" (defaultArg cat.Description "")
|
|> addToHash "subtitle" (defaultArg cat.Description "")
|
||||||
|> addToHash "is_category" true
|
|> addToHash ViewContext.IsCategory true
|
||||||
|> addToHash "is_category_home" (pageNbr = 1)
|
|> addToHash ViewContext.IsCategoryHome (pageNbr = 1)
|
||||||
|> addToHash "slug" slug
|
|> addToHash ViewContext.Slug slug
|
||||||
|> themedView "index" next ctx
|
|> themedView "index" next ctx
|
||||||
| _ -> return! Error.notFound next ctx
|
| _ -> return! Error.notFound next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
|
@ -166,13 +168,13 @@ let pageOfTaggedPosts slugAndPage : HttpHandler = fun next ctx -> task {
|
||||||
else
|
else
|
||||||
match! data.Post.FindPageOfTaggedPosts webLog.Id tag pageNbr webLog.PostsPerPage with
|
match! data.Post.FindPageOfTaggedPosts webLog.Id tag pageNbr webLog.PostsPerPage with
|
||||||
| posts when List.length posts > 0 ->
|
| posts when List.length posts > 0 ->
|
||||||
let! hash = preparePostList webLog posts TagList rawTag pageNbr webLog.PostsPerPage ctx data
|
let! hash = preparePostList webLog posts TagList rawTag pageNbr webLog.PostsPerPage data
|
||||||
let pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>"""
|
let pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>"""
|
||||||
return!
|
return!
|
||||||
addToHash "page_title" $"Posts Tagged “{tag}”{pgTitle}" hash
|
addToHash ViewContext.PageTitle $"Posts Tagged “{tag}”{pgTitle}" hash
|
||||||
|> addToHash "is_tag" true
|
|> addToHash ViewContext.IsTag true
|
||||||
|> addToHash "is_tag_home" (pageNbr = 1)
|
|> addToHash ViewContext.IsTagHome (pageNbr = 1)
|
||||||
|> addToHash "slug" rawTag
|
|> addToHash ViewContext.Slug rawTag
|
||||||
|> themedView "index" next ctx
|
|> themedView "index" next ctx
|
||||||
// Other systems use hyphens for spaces; redirect if this is an old tag link
|
// Other systems use hyphens for spaces; redirect if this is an old tag link
|
||||||
| _ ->
|
| _ ->
|
||||||
|
@ -196,13 +198,11 @@ let home : HttpHandler = fun next ctx -> task {
|
||||||
| pageId ->
|
| pageId ->
|
||||||
match! ctx.Data.Page.FindById (PageId pageId) webLog.Id with
|
match! ctx.Data.Page.FindById (PageId pageId) webLog.Id with
|
||||||
| Some page ->
|
| Some page ->
|
||||||
return! {|
|
return!
|
||||||
page_title = page.Title
|
hashForPage page.Title
|
||||||
page = DisplayPage.fromPage webLog page
|
|> addToHash "page" (DisplayPage.fromPage webLog page)
|
||||||
categories = CategoryCache.get ctx
|
|> addToHash ViewContext.IsHome true
|
||||||
is_home = true
|
|> themedView (defaultArg page.Template "single-page") next ctx
|
||||||
|}
|
|
||||||
|> makeHash |> themedView (defaultArg page.Template "single-page") next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,10 +211,10 @@ let home : HttpHandler = fun next ctx -> task {
|
||||||
let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
let! posts = data.Post.FindPageOfPosts ctx.WebLog.Id pageNbr 25
|
let! posts = data.Post.FindPageOfPosts ctx.WebLog.Id pageNbr 25
|
||||||
let! hash = preparePostList ctx.WebLog posts AdminList "" pageNbr 25 ctx data
|
let! hash = preparePostList ctx.WebLog posts AdminList "" pageNbr 25 data
|
||||||
return!
|
return!
|
||||||
addToHash "page_title" "Posts" hash
|
addToHash ViewContext.PageTitle "Posts" hash
|
||||||
|> addToHash "csrf" ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
|> adminView "post-list" next ctx
|
|> adminView "post-list" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,25 +231,23 @@ let edit postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
}
|
}
|
||||||
match result with
|
match result with
|
||||||
| Some (title, post) when canEdit post.AuthorId ctx ->
|
| Some (title, post) when canEdit post.AuthorId ctx ->
|
||||||
let! cats = data.Category.FindAllForView ctx.WebLog.Id
|
|
||||||
let! templates = templatesForTheme ctx "post"
|
let! templates = templatesForTheme ctx "post"
|
||||||
let model = EditPostModel.fromPost ctx.WebLog post
|
let model = EditPostModel.fromPost ctx.WebLog post
|
||||||
return! {|
|
return!
|
||||||
page_title = title
|
hashForPage title
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = model
|
|> addToHash ViewContext.Model model
|
||||||
metadata = Array.zip model.MetaNames model.MetaValues
|
|> addToHash "metadata" (
|
||||||
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
|
Array.zip model.MetaNames model.MetaValues
|
||||||
templates = templates
|
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |]))
|
||||||
categories = cats
|
|> addToHash "templates" templates
|
||||||
explicit_values = [|
|
|> addToHash "explicit_values" [|
|
||||||
KeyValuePair.Create ("", "– Default –")
|
KeyValuePair.Create ("", "– Default –")
|
||||||
KeyValuePair.Create (ExplicitRating.toString Yes, "Yes")
|
KeyValuePair.Create (ExplicitRating.toString Yes, "Yes")
|
||||||
KeyValuePair.Create (ExplicitRating.toString No, "No")
|
KeyValuePair.Create (ExplicitRating.toString No, "No")
|
||||||
KeyValuePair.Create (ExplicitRating.toString Clean, "Clean")
|
KeyValuePair.Create (ExplicitRating.toString Clean, "Clean")
|
||||||
|]
|
|]
|
||||||
|}
|
|> adminView "post-edit" next ctx
|
||||||
|> makeHash |> adminView "post-edit" next ctx
|
|
||||||
| Some _ -> return! Error.notAuthorized next ctx
|
| Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
@ -266,12 +264,11 @@ let delete postId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
||||||
let editPermalinks postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let editPermalinks postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
|
match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
|
||||||
| Some post when canEdit post.AuthorId ctx ->
|
| Some post when canEdit post.AuthorId ctx ->
|
||||||
return! {|
|
return!
|
||||||
page_title = "Manage Prior Permalinks"
|
hashForPage "Manage Prior Permalinks"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = ManagePermalinksModel.fromPost post
|
|> addToHash ViewContext.Model (ManagePermalinksModel.fromPost post)
|
||||||
|}
|
|> adminView "permalinks" next ctx
|
||||||
|> makeHash |> adminView "permalinks" next ctx
|
|
||||||
| Some _ -> return! Error.notAuthorized next ctx
|
| Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
@ -296,12 +293,11 @@ let savePermalinks : HttpHandler = requireAccess Author >=> fun next ctx -> task
|
||||||
let editRevisions postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let editRevisions postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
|
match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
|
||||||
| Some post when canEdit post.AuthorId ctx ->
|
| Some post when canEdit post.AuthorId ctx ->
|
||||||
return! {|
|
return!
|
||||||
page_title = "Manage Post Revisions"
|
hashForPage "Manage Post Revisions"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = ManageRevisionsModel.fromPost ctx.WebLog post
|
|> addToHash ViewContext.Model (ManageRevisionsModel.fromPost ctx.WebLog post)
|
||||||
|}
|
|> adminView "revisions" next ctx
|
||||||
|> makeHash |> adminView "revisions" next ctx
|
|
||||||
| Some _ -> return! Error.notAuthorized next ctx
|
| Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,9 @@ module CatchAll =
|
||||||
match data.Post.FindByPermalink permalink webLog.Id |> await with
|
match data.Post.FindByPermalink permalink webLog.Id |> await with
|
||||||
| Some post ->
|
| Some post ->
|
||||||
debug (fun () -> "Found post by permalink")
|
debug (fun () -> "Found post by permalink")
|
||||||
let hash = Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 ctx data |> await
|
let hash = Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 data |> await
|
||||||
yield fun next ctx ->
|
yield fun next ctx ->
|
||||||
addToHash "page_title" post.Title hash
|
addToHash ViewContext.PageTitle post.Title hash
|
||||||
|> themedView (defaultArg post.Template "single-post") next ctx
|
|> themedView (defaultArg post.Template "single-post") next ctx
|
||||||
| None -> ()
|
| None -> ()
|
||||||
// Current page
|
// Current page
|
||||||
|
@ -39,13 +39,10 @@ module CatchAll =
|
||||||
| Some page ->
|
| Some page ->
|
||||||
debug (fun () -> "Found page by permalink")
|
debug (fun () -> "Found page by permalink")
|
||||||
yield fun next ctx ->
|
yield fun next ctx ->
|
||||||
{|
|
hashForPage page.Title
|
||||||
page_title = page.Title
|
|> addToHash "page" (DisplayPage.fromPage webLog page)
|
||||||
page = DisplayPage.fromPage webLog page
|
|> addToHash ViewContext.IsPage true
|
||||||
categories = CategoryCache.get ctx
|
|> themedView (defaultArg page.Template "single-page") next ctx
|
||||||
is_page = true
|
|
||||||
|}
|
|
||||||
|> makeHash |> themedView (defaultArg page.Template "single-page") next ctx
|
|
||||||
| None -> ()
|
| None -> ()
|
||||||
// RSS feed
|
// RSS feed
|
||||||
match Feed.deriveFeedType ctx textLink with
|
match Feed.deriveFeedType ctx textLink with
|
||||||
|
@ -195,8 +192,9 @@ let router : HttpHandler = choose [
|
||||||
routef "/%s/delete" Upload.deleteFromDb
|
routef "/%s/delete" Upload.deleteFromDb
|
||||||
])
|
])
|
||||||
subRoute "/user" (choose [
|
subRoute "/user" (choose [
|
||||||
route "/my-info" >=> User.saveMyInfo
|
route "/my-info" >=> User.saveMyInfo
|
||||||
route "/save" >=> User.save
|
route "/save" >=> User.save
|
||||||
|
routef "/%s/delete" User.delete
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
|
|
@ -118,22 +118,19 @@ let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
|> List.map (DisplayUpload.fromUpload webLog Database)
|
|> List.map (DisplayUpload.fromUpload webLog Database)
|
||||||
|> List.append diskUploads
|
|> List.append diskUploads
|
||||||
|> List.sortByDescending (fun file -> file.UpdatedOn, file.Path)
|
|> List.sortByDescending (fun file -> file.UpdatedOn, file.Path)
|
||||||
|
return!
|
||||||
return! {|
|
hashForPage "Uploaded Files"
|
||||||
page_title = "Uploaded Files"
|
|> withAntiCsrf ctx
|
||||||
csrf = ctx.CsrfTokenSet
|
|> addToHash "files" allFiles
|
||||||
files = allFiles
|
|> adminView "upload-list" next ctx
|
||||||
|}
|
|
||||||
|> makeHash |> adminView "upload-list" next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/upload/new
|
// GET /admin/upload/new
|
||||||
let showNew : HttpHandler = requireAccess Author >=> fun next ctx ->
|
let showNew : HttpHandler = requireAccess Author >=> fun next ctx ->
|
||||||
{| page_title = "Upload a File"
|
hashForPage "Upload a File"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
destination = UploadDestination.toString ctx.WebLog.Uploads
|
|> addToHash "destination" (UploadDestination.toString ctx.WebLog.Uploads)
|
||||||
|}
|
|> adminView "upload-new" next ctx
|
||||||
|> makeHash |> adminView "upload-new" next ctx
|
|
||||||
|
|
||||||
|
|
||||||
/// Redirect to the upload list
|
/// Redirect to the upload list
|
||||||
|
|
|
@ -23,11 +23,10 @@ let logOn returnUrl : HttpHandler = fun next ctx ->
|
||||||
match returnUrl with
|
match returnUrl with
|
||||||
| Some _ -> returnUrl
|
| Some _ -> returnUrl
|
||||||
| None -> if ctx.Request.Query.ContainsKey "returnUrl" then Some ctx.Request.Query["returnUrl"].[0] else None
|
| None -> if ctx.Request.Query.ContainsKey "returnUrl" then Some ctx.Request.Query["returnUrl"].[0] else None
|
||||||
{| page_title = "Log On"
|
hashForPage "Log On"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
model = { LogOnModel.empty with ReturnTo = returnTo }
|
|> addToHash ViewContext.Model { LogOnModel.empty with ReturnTo = returnTo }
|
||||||
|}
|
|> adminView "log-on" next ctx
|
||||||
|> makeHash |> adminView "log-on" next ctx
|
|
||||||
|
|
||||||
|
|
||||||
open System.Security.Claims
|
open System.Security.Claims
|
||||||
|
@ -72,21 +71,22 @@ let logOff : HttpHandler = fun next ctx -> task {
|
||||||
// ~~ ADMINISTRATION ~~
|
// ~~ ADMINISTRATION ~~
|
||||||
|
|
||||||
open System.Collections.Generic
|
open System.Collections.Generic
|
||||||
open DotLiquid
|
|
||||||
open Giraffe.Htmx
|
open Giraffe.Htmx
|
||||||
open Microsoft.AspNetCore.Http
|
open Microsoft.AspNetCore.Http
|
||||||
|
|
||||||
/// Create the hash needed to display the user list
|
/// Create the hash needed to display the user list
|
||||||
let private userListHash (ctx : HttpContext) = task {
|
let private userListHash (ctx : HttpContext) = task {
|
||||||
let! users = ctx.Data.WebLogUser.FindByWebLog ctx.WebLog.Id
|
let! users = ctx.Data.WebLogUser.FindByWebLog ctx.WebLog.Id
|
||||||
return makeHash {|
|
return!
|
||||||
page_title = "User Administration"
|
hashForPage "User Administration"
|
||||||
csrf = ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
web_log = ctx.WebLog
|
|> addToHash "users" (users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList)
|
||||||
users = users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList
|
|> addViewContext ctx
|
||||||
|}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Got no time for URL/form manipulators...
|
||||||
|
let private goAway : HttpHandler = RequestErrors.BAD_REQUEST "really?"
|
||||||
|
|
||||||
// GET /admin/users
|
// GET /admin/users
|
||||||
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
let! hash = userListHash ctx
|
let! hash = userListHash ctx
|
||||||
|
@ -103,16 +103,17 @@ let bare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show the edit user page
|
/// Show the edit user page
|
||||||
let private showEdit (hash : Hash) : HttpHandler = fun next ctx ->
|
let private showEdit (model : EditUserModel) : HttpHandler = fun next ctx ->
|
||||||
addToHash "page_title" (if (hash["model"] :?> EditUserModel).IsNew then "Add a New User" else "Edit User") hash
|
hashForPage (if model.IsNew then "Add a New User" else "Edit User")
|
||||||
|> addToHash "csrf" ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
|> addToHash "access_levels"
|
|> addToHash ViewContext.Model model
|
||||||
[| KeyValuePair.Create (AccessLevel.toString Author, "Author")
|
|> addToHash "access_levels" [|
|
||||||
KeyValuePair.Create (AccessLevel.toString Editor, "Editor")
|
KeyValuePair.Create (AccessLevel.toString Author, "Author")
|
||||||
KeyValuePair.Create (AccessLevel.toString WebLogAdmin, "Web Log Admin")
|
KeyValuePair.Create (AccessLevel.toString Editor, "Editor")
|
||||||
if ctx.HasAccessLevel Administrator then
|
KeyValuePair.Create (AccessLevel.toString WebLogAdmin, "Web Log Admin")
|
||||||
KeyValuePair.Create (AccessLevel.toString Administrator, "Administrator")
|
if ctx.HasAccessLevel Administrator then
|
||||||
|]
|
KeyValuePair.Create (AccessLevel.toString Administrator, "Administrator")
|
||||||
|
|]
|
||||||
|> adminBareView "user-edit" next ctx
|
|> adminBareView "user-edit" next ctx
|
||||||
|
|
||||||
// GET /admin/user/{id}/edit
|
// GET /admin/user/{id}/edit
|
||||||
|
@ -123,7 +124,7 @@ let edit usrId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> tas
|
||||||
if isNew then someTask { WebLogUser.empty with Id = userId }
|
if isNew then someTask { WebLogUser.empty with Id = userId }
|
||||||
else ctx.Data.WebLogUser.FindById userId ctx.WebLog.Id
|
else ctx.Data.WebLogUser.FindById userId ctx.WebLog.Id
|
||||||
match! tryUser with
|
match! tryUser with
|
||||||
| Some user -> return! showEdit (makeHash {| model = EditUserModel.fromUser user |}) next ctx
|
| Some user -> return! showEdit (EditUserModel.fromUser user) next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +144,7 @@ let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
| Some user when model.Password = model.PasswordConfirm ->
|
| Some user when model.Password = model.PasswordConfirm ->
|
||||||
let updatedUser = model.UpdateUser user
|
let updatedUser = model.UpdateUser user
|
||||||
if updatedUser.AccessLevel = Administrator && not (ctx.HasAccessLevel Administrator) then
|
if updatedUser.AccessLevel = Administrator && not (ctx.HasAccessLevel Administrator) then
|
||||||
return! RequestErrors.BAD_REQUEST "really?" next ctx
|
return! goAway next ctx
|
||||||
else
|
else
|
||||||
let updatedUser =
|
let updatedUser =
|
||||||
if model.Password = "" then updatedUser
|
if model.Password = "" then updatedUser
|
||||||
|
@ -159,26 +160,51 @@ let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
| Some _ ->
|
| Some _ ->
|
||||||
do! addMessage ctx { UserMessage.error with Message = "The passwords did not match; nothing saved" }
|
do! addMessage ctx { UserMessage.error with Message = "The passwords did not match; nothing saved" }
|
||||||
return!
|
return!
|
||||||
(withHxRetarget $"#user_{model.Id}"
|
(withHxRetarget $"#user_{model.Id}" >=> showEdit { model with Password = ""; PasswordConfirm = "" })
|
||||||
>=> showEdit (makeHash {| model = { model with Password = ""; PasswordConfirm = "" } |}))
|
|
||||||
next ctx
|
next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /admin/user/{id}/delete
|
||||||
|
let delete userId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||||
|
let data = ctx.Data
|
||||||
|
match! data.WebLogUser.FindById (WebLogUserId userId) ctx.WebLog.Id with
|
||||||
|
| Some user ->
|
||||||
|
if user.AccessLevel = Administrator && not (ctx.HasAccessLevel Administrator) then
|
||||||
|
return! goAway next ctx
|
||||||
|
else
|
||||||
|
match! data.WebLogUser.Delete user.Id user.WebLogId with
|
||||||
|
| Ok _ ->
|
||||||
|
do! addMessage ctx
|
||||||
|
{ UserMessage.success with
|
||||||
|
Message = $"User {WebLogUser.displayName user} deleted successfully"
|
||||||
|
}
|
||||||
|
return! bare next ctx
|
||||||
|
| Error msg ->
|
||||||
|
do! addMessage ctx
|
||||||
|
{ UserMessage.error with
|
||||||
|
Message = $"User {WebLogUser.displayName user} was not deleted"
|
||||||
|
Detail = Some msg
|
||||||
|
}
|
||||||
|
return! bare next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
/// Display the user "my info" page, with information possibly filled in
|
/// Display the user "my info" page, with information possibly filled in
|
||||||
let private showMyInfo (user : WebLogUser) (hash : Hash) : HttpHandler = fun next ctx ->
|
let private showMyInfo (model : EditMyInfoModel) (user : WebLogUser) : HttpHandler = fun next ctx ->
|
||||||
addToHash "page_title" "Edit Your Information" hash
|
hashForPage "Edit Your Information"
|
||||||
|> addToHash "csrf" ctx.CsrfTokenSet
|
|> withAntiCsrf ctx
|
||||||
|> addToHash "access_level" (AccessLevel.toString user.AccessLevel)
|
|> addToHash ViewContext.Model model
|
||||||
|> addToHash "created_on" (WebLog.localTime ctx.WebLog user.CreatedOn)
|
|> addToHash "access_level" (AccessLevel.toString user.AccessLevel)
|
||||||
|> addToHash "last_seen_on" (WebLog.localTime ctx.WebLog (defaultArg user.LastSeenOn DateTime.UnixEpoch))
|
|> addToHash "created_on" (WebLog.localTime ctx.WebLog user.CreatedOn)
|
||||||
|
|> addToHash "last_seen_on" (WebLog.localTime ctx.WebLog (defaultArg user.LastSeenOn DateTime.UnixEpoch))
|
||||||
|> adminView "my-info" next ctx
|
|> adminView "my-info" next ctx
|
||||||
|
|
||||||
|
|
||||||
// GET /admin/user/my-info
|
// GET /admin/user/my-info
|
||||||
let myInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let myInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
match! ctx.Data.WebLogUser.FindById ctx.UserId ctx.WebLog.Id with
|
match! ctx.Data.WebLogUser.FindById ctx.UserId ctx.WebLog.Id with
|
||||||
| Some user -> return! showMyInfo user (makeHash {| model = EditMyInfoModel.fromUser user |}) next ctx
|
| Some user -> return! showMyInfo (EditMyInfoModel.fromUser user) user next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +234,6 @@ let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
return! redirectToGet "admin/user/my-info" next ctx
|
return! redirectToGet "admin/user/my-info" next ctx
|
||||||
| Some user ->
|
| Some user ->
|
||||||
do! addMessage ctx { UserMessage.error with Message = "Passwords did not match; no updates made" }
|
do! addMessage ctx { UserMessage.error with Message = "Passwords did not match; no updates made" }
|
||||||
return! showMyInfo user (makeHash {| model = { model with NewPassword = ""; NewPasswordConfirm = "" } |})
|
return! showMyInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user next ctx
|
||||||
next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ module DataImplementation =
|
||||||
let createSQLite connStr =
|
let createSQLite connStr =
|
||||||
let log = sp.GetRequiredService<ILogger<SQLiteData>> ()
|
let log = sp.GetRequiredService<ILogger<SQLiteData>> ()
|
||||||
let conn = new SqliteConnection (connStr)
|
let conn = new SqliteConnection (connStr)
|
||||||
log.LogInformation $"Using SQL database {conn.DataSource}"
|
log.LogInformation $"Using SQLite database {conn.DataSource}"
|
||||||
await (SQLiteData.setUpConnection conn)
|
await (SQLiteData.setUpConnection conn)
|
||||||
SQLiteData (conn, log)
|
SQLiteData (conn, log)
|
||||||
|
|
||||||
|
@ -62,6 +62,26 @@ module DataImplementation =
|
||||||
upcast createSQLite "Data Source=./myweblog.db;Cache=Shared"
|
upcast createSQLite "Data Source=./myweblog.db;Cache=Shared"
|
||||||
|
|
||||||
|
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
|
/// Show a list of valid command-line interface commands
|
||||||
|
let showHelp () =
|
||||||
|
printfn " "
|
||||||
|
printfn "COMMAND WHAT IT DOES"
|
||||||
|
printfn "----------- ------------------------------------------------------"
|
||||||
|
printfn "backup Create a JSON file backup of a web log"
|
||||||
|
printfn "do-restore Restore a JSON file backup (overwrite data silently)"
|
||||||
|
printfn "help Display this information"
|
||||||
|
printfn "import-links Import prior permalinks"
|
||||||
|
printfn "init Initializes a new web log"
|
||||||
|
printfn "load-theme Load a theme"
|
||||||
|
printfn "restore Restore a JSON file backup (prompt before overwriting)"
|
||||||
|
printfn "upgrade-user Upgrade a WebLogAdmin user to a full Administrator"
|
||||||
|
printfn " "
|
||||||
|
printfn "For more information on a particular command, run it with no options."
|
||||||
|
Task.FromResult ()
|
||||||
|
|
||||||
|
|
||||||
open Giraffe
|
open Giraffe
|
||||||
open Giraffe.EndpointRouting
|
open Giraffe.EndpointRouting
|
||||||
open Microsoft.AspNetCore.Authentication.Cookies
|
open Microsoft.AspNetCore.Authentication.Cookies
|
||||||
|
@ -138,7 +158,11 @@ let rec main args =
|
||||||
| Some it when it = "restore" -> Maintenance.Backup.restoreFromBackup args app.Services
|
| Some it when it = "restore" -> Maintenance.Backup.restoreFromBackup args app.Services
|
||||||
| Some it when it = "do-restore" -> Maintenance.Backup.restoreFromBackup args app.Services
|
| Some it when it = "do-restore" -> Maintenance.Backup.restoreFromBackup args app.Services
|
||||||
| Some it when it = "upgrade-user" -> Maintenance.upgradeUser args app.Services
|
| Some it when it = "upgrade-user" -> Maintenance.upgradeUser args app.Services
|
||||||
| _ ->
|
| Some it when it = "help" -> showHelp ()
|
||||||
|
| Some it ->
|
||||||
|
printfn $"""Unrecognized command "{it}" - valid commands are:"""
|
||||||
|
showHelp ()
|
||||||
|
| None ->
|
||||||
let _ = app.UseForwardedHeaders ()
|
let _ = app.UseForwardedHeaders ()
|
||||||
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
||||||
let _ = app.UseMiddleware<WebLogMiddleware> ()
|
let _ = app.UseMiddleware<WebLogMiddleware> ()
|
||||||
|
@ -148,7 +172,7 @@ let rec main args =
|
||||||
let _ = app.UseSession ()
|
let _ = app.UseSession ()
|
||||||
let _ = app.UseGiraffe Handlers.Routes.endpoint
|
let _ = app.UseGiraffe Handlers.Routes.endpoint
|
||||||
|
|
||||||
System.Threading.Tasks.Task.FromResult (app.Run ())
|
Task.FromResult (app.Run ())
|
||||||
|> Async.AwaitTask |> Async.RunSynchronously
|
|> Async.AwaitTask |> Async.RunSynchronously
|
||||||
|
|
||||||
0 // Exit code
|
0 // Exit code
|
||||||
|
|
|
@ -19,19 +19,23 @@
|
||||||
{%- elsif user.access_level == "Author" %}
|
{%- elsif user.access_level == "Author" %}
|
||||||
<span class="{{ badge }}-dark">AUTHOR</span>
|
<span class="{{ badge }}-dark">AUTHOR</span>
|
||||||
{%- endif %}<br>
|
{%- endif %}<br>
|
||||||
<small>
|
{%- unless is_administrator == false and user.access_level == "Administrator" %}
|
||||||
{%- assign user_url_base = "admin/user/" | append: user.id -%}
|
<small>
|
||||||
<a href="{{ user_url_base | append: "/edit" | relative_link }}" hx-target="#user_{{ user.id }}"
|
{%- assign user_url_base = "admin/user/" | append: user.id -%}
|
||||||
hx-swap="innerHTML show:#user_{{ user.id }}:top">
|
<a href="{{ user_url_base | append: "/edit" | relative_link }}" hx-target="#user_{{ user.id }}"
|
||||||
Edit
|
hx-swap="innerHTML show:#user_{{ user.id }}:top">
|
||||||
</a>
|
Edit
|
||||||
<span class="text-muted"> • </span>
|
</a>
|
||||||
{%- assign user_del_link = user_url_base | append: "/delete" | relative_link -%}
|
{% unless user_id == user.id %}
|
||||||
<a href="{{ user_del_link }}" hx-post="{{ user_del_link }}" class="text-danger"
|
<span class="text-muted"> • </span>
|
||||||
hx-confirm="Are you sure you want to delete the category “{{ cat.name }}”? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)">
|
{%- assign user_del_link = user_url_base | append: "/delete" | relative_link -%}
|
||||||
Delete
|
<a href="{{ user_del_link }}" hx-post="{{ user_del_link }}" class="text-danger"
|
||||||
</a>
|
hx-confirm="Are you sure you want to delete the user “{{ user.preferred_name }}”? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)">
|
||||||
</small>
|
Delete
|
||||||
|
</a>
|
||||||
|
{% endunless %}
|
||||||
|
</small>
|
||||||
|
{%- endunless %}
|
||||||
</div>
|
</div>
|
||||||
<div class="{{ email_col }}">
|
<div class="{{ email_col }}">
|
||||||
{{ user.first_name }} {{ user.last_name }}<br>
|
{{ user.first_name }} {{ user.last_name }}<br>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user