Add access restrictions to UI (#19)
- Vary default user access for new web logs (#19) - Add htmx detection to not auth/404 handlers - Bump version
This commit is contained in:
parent
eae1509d81
commit
d30312c23f
|
@ -92,6 +92,9 @@ type DisplayPage =
|
||||||
{ /// The ID of this page
|
{ /// The ID of this page
|
||||||
id : string
|
id : string
|
||||||
|
|
||||||
|
/// The ID of the author of this page
|
||||||
|
authorId : string
|
||||||
|
|
||||||
/// The title of the page
|
/// The title of the page
|
||||||
title : string
|
title : string
|
||||||
|
|
||||||
|
@ -121,6 +124,7 @@ type DisplayPage =
|
||||||
static member fromPageMinimal webLog (page : Page) =
|
static member fromPageMinimal webLog (page : Page) =
|
||||||
let pageId = PageId.toString page.id
|
let pageId = PageId.toString page.id
|
||||||
{ id = pageId
|
{ id = pageId
|
||||||
|
authorId = WebLogUserId.toString page.authorId
|
||||||
title = page.title
|
title = page.title
|
||||||
permalink = Permalink.toString page.permalink
|
permalink = Permalink.toString page.permalink
|
||||||
publishedOn = page.publishedOn
|
publishedOn = page.publishedOn
|
||||||
|
@ -136,6 +140,7 @@ type DisplayPage =
|
||||||
let _, extra = WebLog.hostAndPath webLog
|
let _, extra = WebLog.hostAndPath webLog
|
||||||
let pageId = PageId.toString page.id
|
let pageId = PageId.toString page.id
|
||||||
{ id = pageId
|
{ id = pageId
|
||||||
|
authorId = WebLogUserId.toString page.authorId
|
||||||
title = page.title
|
title = page.title
|
||||||
permalink = Permalink.toString page.permalink
|
permalink = Permalink.toString page.permalink
|
||||||
publishedOn = page.publishedOn
|
publishedOn = page.publishedOn
|
||||||
|
|
|
@ -188,7 +188,7 @@ type UserLinksTag () =
|
||||||
let link it = WebLog.relativeUrl webLog (Permalink it)
|
let link it = WebLog.relativeUrl webLog (Permalink it)
|
||||||
seq {
|
seq {
|
||||||
"""<ul class="navbar-nav flex-grow-1 justify-content-end">"""
|
"""<ul class="navbar-nav flex-grow-1 justify-content-end">"""
|
||||||
match Convert.ToBoolean context.Environments[0].["logged_on"] with
|
match Convert.ToBoolean context.Environments[0].["is_logged_on"] with
|
||||||
| true ->
|
| true ->
|
||||||
$"""<li class="nav-item"><a class="nav-link" href="{link "admin/dashboard"}">Dashboard</a></li>"""
|
$"""<li class="nav-item"><a class="nav-link" href="{link "admin/dashboard"}">Dashboard</a></li>"""
|
||||||
$"""<li class="nav-item"><a class="nav-link" href="{link "user/log-off"}">Log Off</a></li>"""
|
$"""<li class="nav-item"><a class="nav-link" href="{link "user/log-off"}">Log Off</a></li>"""
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
/// Handlers for error conditions
|
|
||||||
module MyWebLog.Handlers.Error
|
|
||||||
|
|
||||||
open System.Net
|
|
||||||
open Giraffe
|
|
||||||
open MyWebLog
|
|
||||||
|
|
||||||
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response
|
|
||||||
let notAuthorized : HttpHandler =
|
|
||||||
handleContext (fun ctx ->
|
|
||||||
if ctx.Request.Method = "GET" then
|
|
||||||
let returnUrl = WebUtility.UrlEncode ctx.Request.Path
|
|
||||||
redirectTo false (WebLog.relativeUrl ctx.WebLog (Permalink $"user/log-on?returnUrl={returnUrl}"))
|
|
||||||
earlyReturn ctx
|
|
||||||
else
|
|
||||||
setStatusCode 401 earlyReturn ctx)
|
|
||||||
|
|
||||||
|
|
||||||
/// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there
|
|
||||||
let notFound : HttpHandler = fun _ ->
|
|
||||||
(setStatusCode 404 >=> text "Not found") earlyReturn
|
|
|
@ -55,11 +55,13 @@ let messages (ctx : HttpContext) = task {
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
open DotLiquid
|
open DotLiquid
|
||||||
|
|
||||||
/// Either get the web log from the hash, or get it from the cache and add it to the hash
|
/// Add a key to the hash, returning the modified hash
|
||||||
let private deriveWebLogFromHash (hash : Hash) (ctx : HttpContext) =
|
// (note that the hash itself is mutated; this is only used to make it pipeable)
|
||||||
if hash.ContainsKey "web_log" then () else hash.Add ("web_log", ctx.WebLog)
|
let addToHash key (value : obj) (hash : Hash) =
|
||||||
hash["web_log"] :?> WebLog
|
hash.Add (key, value)
|
||||||
|
hash
|
||||||
|
|
||||||
|
open System.Security.Claims
|
||||||
open Giraffe
|
open Giraffe
|
||||||
open Giraffe.Htmx
|
open Giraffe.Htmx
|
||||||
open Giraffe.ViewEngine
|
open Giraffe.ViewEngine
|
||||||
|
@ -69,51 +71,57 @@ 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 private populateHash hash ctx = task {
|
||||||
// Don't need the web log, but this adds it to the hash if the function is called directly
|
|
||||||
let _ = deriveWebLogFromHash hash ctx
|
|
||||||
let! messages = messages ctx
|
let! messages = messages ctx
|
||||||
hash.Add ("logged_on", ctx.User.Identity.IsAuthenticated)
|
|
||||||
hash.Add ("page_list", PageListCache.get ctx)
|
|
||||||
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
|
|
||||||
hash.Add ("messages", messages)
|
|
||||||
hash.Add ("generator", ctx.Generator)
|
|
||||||
hash.Add ("htmx_script", htmxScript)
|
|
||||||
|
|
||||||
do! commitSession ctx
|
do! commitSession ctx
|
||||||
|
|
||||||
|
let accessLevel = ctx.UserAccessLevel
|
||||||
|
let hasLevel lvl = accessLevel |> Option.map (AccessLevel.hasAccess lvl) |> Option.defaultValue false
|
||||||
|
|
||||||
|
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
|
||||||
|
addToHash "web_log" ctx.WebLog hash
|
||||||
|
|> addToHash "page_list" (PageListCache.get ctx)
|
||||||
|
|> addToHash "current_page" ctx.Request.Path.Value[1..]
|
||||||
|
|> addToHash "messages" messages
|
||||||
|
|> addToHash "generator" ctx.Generator
|
||||||
|
|> addToHash "htmx_script" htmxScript
|
||||||
|
|> addToHash "is_logged_on" ctx.User.Identity.IsAuthenticated
|
||||||
|
|> addToHash "is_author" (hasLevel Author)
|
||||||
|
|> addToHash "is_editor" (hasLevel Editor)
|
||||||
|
|> addToHash "is_web_log_admin" (hasLevel WebLogAdmin)
|
||||||
|
|> addToHash "is_administrator" (hasLevel Administrator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is the request from htmx?
|
||||||
|
let isHtmx (ctx : HttpContext) =
|
||||||
|
ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
|
||||||
|
|
||||||
/// 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 theme template next ctx (hash : Hash) = task {
|
let viewForTheme theme template next ctx (hash : Hash) = task {
|
||||||
do! populateHash hash ctx
|
if not (hash.ContainsKey "web_log") then
|
||||||
|
let! _ = populateHash hash ctx
|
||||||
|
()
|
||||||
|
|
||||||
// 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
|
||||||
hash.Add ("content", contentTemplate.Render hash)
|
let _ = addToHash "content" (contentTemplate.Render hash) hash
|
||||||
|
|
||||||
// ...then render that content with its layout
|
// ...then render that content with its layout
|
||||||
let isHtmx = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
|
let! layoutTemplate = TemplateCache.get theme (if isHtmx ctx then "layout-partial" else "layout") ctx.Data
|
||||||
let! layoutTemplate = TemplateCache.get theme (if isHtmx then "layout-partial" else "layout") ctx.Data
|
|
||||||
|
|
||||||
return! htmlString (layoutTemplate.Render hash) next ctx
|
return! htmlString (layoutTemplate.Render hash) next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a bare view for the specified theme, using the specified template and hash
|
/// Convert messages to headers (used for htmx responses)
|
||||||
let bareForTheme theme template next ctx (hash : Hash) = task {
|
let messagesToHeaders (messages : UserMessage array) : HttpHandler =
|
||||||
do! populateHash hash ctx
|
seq {
|
||||||
|
|
||||||
if not (hash.ContainsKey "content") then
|
|
||||||
let! contentTemplate = TemplateCache.get theme template ctx.Data
|
|
||||||
hash.Add ("content", contentTemplate.Render hash)
|
|
||||||
|
|
||||||
// Bare templates are rendered with layout-bare
|
|
||||||
let! layoutTemplate = TemplateCache.get theme "layout-bare" ctx.Data
|
|
||||||
|
|
||||||
// add messages as HTTP headers
|
|
||||||
let messages = hash["messages"] :?> UserMessage[]
|
|
||||||
let actions = seq {
|
|
||||||
yield!
|
yield!
|
||||||
messages
|
messages
|
||||||
|> Array.map (fun m ->
|
|> Array.map (fun m ->
|
||||||
|
@ -122,15 +130,29 @@ let bareForTheme theme template next ctx (hash : Hash) = task {
|
||||||
| None -> $"{m.level}|||{m.message}"
|
| None -> $"{m.level}|||{m.message}"
|
||||||
|> setHttpHeader "X-Message")
|
|> setHttpHeader "X-Message")
|
||||||
withHxNoPushUrl
|
withHxNoPushUrl
|
||||||
htmlString (layoutTemplate.Render hash)
|
}
|
||||||
}
|
|> Seq.reduce (>=>)
|
||||||
|
|
||||||
return! (actions |> Seq.reduce (>=>)) next ctx
|
/// Render a bare view for the specified theme, using the specified template and hash
|
||||||
|
let bareForTheme theme template next ctx (hash : Hash) = task {
|
||||||
|
let! hash = populateHash hash ctx
|
||||||
|
|
||||||
|
if not (hash.ContainsKey "content") then
|
||||||
|
let! contentTemplate = TemplateCache.get theme template ctx.Data
|
||||||
|
addToHash "content" (contentTemplate.Render hash) hash |> ignore
|
||||||
|
|
||||||
|
// Bare templates are rendered with layout-bare
|
||||||
|
let! layoutTemplate = TemplateCache.get theme "layout-bare" ctx.Data
|
||||||
|
|
||||||
|
return!
|
||||||
|
(messagesToHeaders (hash["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 =
|
let themedView template next ctx hash = task {
|
||||||
viewForTheme (deriveWebLogFromHash hash ctx).themePath template next ctx hash
|
let! hash = populateHash hash ctx
|
||||||
|
return! viewForTheme (hash["web_log"] :?> WebLog).themePath template next ctx hash
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Redirect after doing some action; commits session and issues a temporary redirect
|
/// Redirect after doing some action; commits session and issues a temporary redirect
|
||||||
|
@ -146,13 +168,59 @@ let validateCsrf : HttpHandler = fun next ctx -> task {
|
||||||
| false -> return! RequestErrors.BAD_REQUEST "CSRF token invalid" earlyReturn ctx
|
| false -> return! RequestErrors.BAD_REQUEST "CSRF token invalid" earlyReturn ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Handlers for error conditions
|
||||||
|
module Error =
|
||||||
|
|
||||||
|
open System.Net
|
||||||
|
|
||||||
|
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response
|
||||||
|
let notAuthorized : HttpHandler = fun next ctx ->
|
||||||
|
if ctx.Request.Method = "GET" then
|
||||||
|
let redirectUrl = $"user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}"
|
||||||
|
if isHtmx ctx then (withHxRedirect redirectUrl >=> redirectToGet redirectUrl) next ctx
|
||||||
|
else redirectToGet redirectUrl next ctx
|
||||||
|
else
|
||||||
|
if isHtmx ctx then
|
||||||
|
let messages = [|
|
||||||
|
{ UserMessage.error with
|
||||||
|
message = $"You are not authorized to access the URL {ctx.Request.Path.Value}"
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
(messagesToHeaders messages >=> setStatusCode 401) earlyReturn ctx
|
||||||
|
else setStatusCode 401 earlyReturn ctx
|
||||||
|
|
||||||
|
/// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there
|
||||||
|
let notFound : HttpHandler =
|
||||||
|
handleContext (fun ctx ->
|
||||||
|
if isHtmx ctx then
|
||||||
|
let messages = [|
|
||||||
|
{ UserMessage.error with message = $"The URL {ctx.Request.Path.Value} was not found" }
|
||||||
|
|]
|
||||||
|
(messagesToHeaders messages >=> setStatusCode 404) earlyReturn ctx
|
||||||
|
else
|
||||||
|
(setStatusCode 404 >=> text "Not found") earlyReturn ctx)
|
||||||
|
|
||||||
|
|
||||||
/// Require a user to be logged on
|
/// Require a user to be logged on
|
||||||
let requireUser : HttpHandler = requiresAuthentication Error.notAuthorized
|
let requireUser : HttpHandler = requiresAuthentication Error.notAuthorized
|
||||||
|
|
||||||
/// Require a specific level of access for a route
|
/// Require a specific level of access for a route
|
||||||
let requireAccess level : HttpHandler = fun next ctx ->
|
let requireAccess level : HttpHandler = fun next ctx -> task {
|
||||||
if defaultArg (ctx.UserAccessLevel |> Option.map (AccessLevel.hasAccess level)) false then next ctx
|
let userLevel = ctx.UserAccessLevel
|
||||||
else Error.notAuthorized next ctx
|
if defaultArg (userLevel |> Option.map (AccessLevel.hasAccess level)) false then
|
||||||
|
return! next ctx
|
||||||
|
else
|
||||||
|
let message =
|
||||||
|
match userLevel with
|
||||||
|
| Some lvl ->
|
||||||
|
$"The page you tried to access requires {AccessLevel.toString level} privileges; your account only has {AccessLevel.toString lvl} privileges"
|
||||||
|
| None -> "The page you tried to access required you to be logged on"
|
||||||
|
do! addMessage ctx { UserMessage.warning with message = message }
|
||||||
|
printfn "Added message to context"
|
||||||
|
do! commitSession ctx
|
||||||
|
return! Error.notAuthorized next ctx
|
||||||
|
}
|
||||||
|
|
||||||
/// Determine if a user is authorized to edit a page or post, given the author
|
/// Determine if a user is authorized to edit a page or post, given the author
|
||||||
let canEdit authorId (ctx : HttpContext) =
|
let canEdit authorId (ctx : HttpContext) =
|
||||||
|
|
|
@ -55,7 +55,10 @@ let doLogOn : HttpHandler = fun next ctx -> task {
|
||||||
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
|
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
|
||||||
do! addMessage ctx
|
do! addMessage ctx
|
||||||
{ UserMessage.success with message = $"Logged on successfully | Welcome to {ctx.WebLog.name}!" }
|
{ UserMessage.success with message = $"Logged on successfully | Welcome to {ctx.WebLog.name}!" }
|
||||||
return! redirectToGet (defaultArg (model.returnTo |> Option.map (fun it -> it[1..])) "admin/dashboard") next ctx
|
return!
|
||||||
|
match model.returnTo with
|
||||||
|
| Some url -> redirectTo false url next ctx
|
||||||
|
| None -> redirectToGet "admin/dashboard" next ctx
|
||||||
| _ ->
|
| _ ->
|
||||||
do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" }
|
do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" }
|
||||||
return! logOn model.returnTo next ctx
|
return! logOn model.returnTo next ctx
|
||||||
|
|
|
@ -25,6 +25,11 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
|
||||||
let homePageId = PageId.create ()
|
let homePageId = PageId.create ()
|
||||||
let slug = Handlers.Upload.makeSlug args[2]
|
let slug = Handlers.Upload.makeSlug args[2]
|
||||||
|
|
||||||
|
// If this is the first web log being created, the user will be an installation admin; otherwise, they will be an
|
||||||
|
// admin just over their web log
|
||||||
|
let! webLogs = data.WebLog.all ()
|
||||||
|
let accessLevel = if List.isEmpty webLogs then Administrator else WebLogAdmin
|
||||||
|
|
||||||
do! data.WebLog.add
|
do! data.WebLog.add
|
||||||
{ WebLog.empty with
|
{ WebLog.empty with
|
||||||
id = webLogId
|
id = webLogId
|
||||||
|
@ -48,7 +53,7 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
|
||||||
preferredName = "Admin"
|
preferredName = "Admin"
|
||||||
passwordHash = Handlers.User.hashedPassword args[4] args[3] salt
|
passwordHash = Handlers.User.hashedPassword args[4] args[3] salt
|
||||||
salt = salt
|
salt = salt
|
||||||
accessLevel = Administrator
|
accessLevel = accessLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the default home page
|
// Create the default home page
|
||||||
|
@ -70,6 +75,12 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
|
||||||
}
|
}
|
||||||
|
|
||||||
printfn $"Successfully initialized database for {args[2]} with URL base {args[1]}"
|
printfn $"Successfully initialized database for {args[2]} with URL base {args[1]}"
|
||||||
|
match accessLevel with
|
||||||
|
| Administrator -> printfn $" ({args[3]} is an installation administrator)"
|
||||||
|
| WebLogAdmin ->
|
||||||
|
printfn $" ({args[3]} is a web log administrator;"
|
||||||
|
printfn """ use "upgrade-user" to promote to installation administrator)"""
|
||||||
|
| _ -> ()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new web log
|
/// Create a new web log
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="appsettings*.json" CopyToOutputDirectory="Always" />
|
<Content Include="appsettings*.json" CopyToOutputDirectory="Always" />
|
||||||
<Compile Include="Caches.fs" />
|
<Compile Include="Caches.fs" />
|
||||||
<Compile Include="Handlers\Error.fs" />
|
|
||||||
<Compile Include="Handlers\Helpers.fs" />
|
<Compile Include="Handlers\Helpers.fs" />
|
||||||
<Compile Include="Handlers\Admin.fs" />
|
<Compile Include="Handlers\Admin.fs" />
|
||||||
<Compile Include="Handlers\Feed.fs" />
|
<Compile Include="Handlers\Feed.fs" />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"Generator": "myWebLog 2.0-beta04",
|
"Generator": "myWebLog 2.0-beta05",
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"MyWebLog.Handlers": "Information"
|
"MyWebLog.Handlers": "Information"
|
||||||
|
|
|
@ -7,20 +7,26 @@
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarText">
|
<div class="collapse navbar-collapse" id="navbarText">
|
||||||
{% if logged_on -%}
|
{% if is_logged_on -%}
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
||||||
{{ "admin/pages" | nav_link: "Pages" }}
|
{% if is_author %}
|
||||||
{{ "admin/posts" | nav_link: "Posts" }}
|
{{ "admin/pages" | nav_link: "Pages" }}
|
||||||
{{ "admin/uploads" | nav_link: "Uploads" }}
|
{{ "admin/posts" | nav_link: "Posts" }}
|
||||||
{{ "admin/categories" | nav_link: "Categories" }}
|
{{ "admin/uploads" | nav_link: "Uploads" }}
|
||||||
{{ "admin/settings" | nav_link: "Settings" }}
|
{% endif %}
|
||||||
|
{% if is_web_log_admin %}
|
||||||
|
{{ "admin/categories" | nav_link: "Categories" }}
|
||||||
|
{{ "admin/settings" | nav_link: "Settings" }}
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
||||||
{% if logged_on -%}
|
{% if is_logged_on -%}
|
||||||
{{ "admin/user/edit" | nav_link: "Edit User" }}
|
{{ "admin/user/edit" | nav_link: "Edit User" }}
|
||||||
{{ "user/log-off" | nav_link: "Log Off" }}
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ "user/log-off" | relative_link }}" hx-boost="false">Log Off</a>
|
||||||
|
</li>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{{ "user/log-on" | nav_link: "Log On" }}
|
{{ "user/log-on" | nav_link: "Log On" }}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
|
@ -13,20 +13,19 @@
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{{ cat.name }}<br>
|
{{ cat.name }}<br>
|
||||||
<small>
|
<small>
|
||||||
|
{%- assign cat_url_base = "admin/category/" | append: cat.id -%}
|
||||||
{%- if cat.post_count > 0 %}
|
{%- if cat.post_count > 0 %}
|
||||||
<a href="{{ cat | category_link }}" target="_blank">
|
<a href="{{ cat | category_link }}" target="_blank">
|
||||||
View {{ cat.post_count }} Post{% unless cat.post_count == 1 %}s{% endunless -%}
|
View {{ cat.post_count }} Post{% unless cat.post_count == 1 %}s{% endunless -%}
|
||||||
</a>
|
</a>
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- capture cat_edit %}admin/category/{{ cat.id }}/edit{% endcapture -%}
|
<a href="{{ cat_url_base | append: "/edit" | relative_link }}" hx-target="#cat_{{ cat.id }}"
|
||||||
<a href="{{ cat_edit | relative_link }}" hx-target="#cat_{{ cat.id }}"
|
|
||||||
hx-swap="innerHTML show:#cat_{{ cat.id }}:top">
|
hx-swap="innerHTML show:#cat_{{ cat.id }}:top">
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- capture cat_del %}admin/category/{{ cat.id }}/delete{% endcapture -%}
|
{%- assign cat_del_link = cat_url_base | append: "/delete" | relative_link -%}
|
||||||
{%- capture cat_del_link %}{{ cat_del | relative_link }}{% endcapture -%}
|
|
||||||
<a href="{{ cat_del_link }}" hx-post="{{ cat_del_link }}" class="text-danger"
|
<a href="{{ cat_del_link }}" hx-post="{{ cat_del_link }}" class="text-danger"
|
||||||
hx-confirm="Are you sure you want to delete the category “{{ cat.name }}”? This action cannot be undone.">
|
hx-confirm="Are you sure you want to delete the category “{{ cat.name }}”? This action cannot be undone.">
|
||||||
Delete
|
Delete
|
||||||
|
|
|
@ -9,8 +9,10 @@
|
||||||
Published <span class="badge rounded-pill bg-secondary">{{ model.posts }}</span>
|
Published <span class="badge rounded-pill bg-secondary">{{ model.posts }}</span>
|
||||||
Drafts <span class="badge rounded-pill bg-secondary">{{ model.drafts }}</span>
|
Drafts <span class="badge rounded-pill bg-secondary">{{ model.drafts }}</span>
|
||||||
</h6>
|
</h6>
|
||||||
<a href="{{ "admin/posts" | relative_link }}" class="btn btn-secondary me-2">View All</a>
|
{% if is_author %}
|
||||||
<a href="{{ "admin/post/new/edit" | relative_link }}" class="btn btn-primary">Write a New Post</a>
|
<a href="{{ "admin/posts" | relative_link }}" class="btn btn-secondary me-2">View All</a>
|
||||||
|
<a href="{{ "admin/post/new/edit" | relative_link }}" class="btn btn-primary">Write a New Post</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -22,8 +24,10 @@
|
||||||
All <span class="badge rounded-pill bg-secondary">{{ model.pages }}</span>
|
All <span class="badge rounded-pill bg-secondary">{{ model.pages }}</span>
|
||||||
Shown in Page List <span class="badge rounded-pill bg-secondary">{{ model.listed_pages }}</span>
|
Shown in Page List <span class="badge rounded-pill bg-secondary">{{ model.listed_pages }}</span>
|
||||||
</h6>
|
</h6>
|
||||||
<a href="{{ "admin/pages" | relative_link }}" class="btn btn-secondary me-2">View All</a>
|
{% if is_author %}
|
||||||
<a href="{{ "admin/page/new/edit" | relative_link }}" class="btn btn-primary">Create a New Page</a>
|
<a href="{{ "admin/pages" | relative_link }}" class="btn btn-secondary me-2">View All</a>
|
||||||
|
<a href="{{ "admin/page/new/edit" | relative_link }}" class="btn btn-primary">Create a New Page</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -37,15 +41,19 @@
|
||||||
All <span class="badge rounded-pill bg-secondary">{{ model.categories }}</span>
|
All <span class="badge rounded-pill bg-secondary">{{ model.categories }}</span>
|
||||||
Top Level <span class="badge rounded-pill bg-secondary">{{ model.top_level_categories }}</span>
|
Top Level <span class="badge rounded-pill bg-secondary">{{ model.top_level_categories }}</span>
|
||||||
</h6>
|
</h6>
|
||||||
<a href="{{ "admin/categories" | relative_link }}" class="btn btn-secondary me-2">View All</a>
|
{% if is_web_log_admin %}
|
||||||
<a href="{{ "admin/category/new/edit" | relative_link }}" class="btn btn-secondary">Add a New Category</a>
|
<a href="{{ "admin/categories" | relative_link }}" class="btn btn-secondary me-2">View All</a>
|
||||||
|
<a href="{{ "admin/category/new/edit" | relative_link }}" class="btn btn-secondary">Add a New Category</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="row pb-3">
|
{% if is_web_log_admin %}
|
||||||
<div class="col text-end">
|
<div class="row pb-3">
|
||||||
<a href="{{ "admin/settings" | relative_link }}" class="btn btn-secondary">Modify Settings</a>
|
<div class="col text-end">
|
||||||
|
<a href="{{ "admin/settings" | relative_link }}" class="btn btn-secondary">Modify Settings</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -24,15 +24,18 @@
|
||||||
<small>
|
<small>
|
||||||
{%- capture pg_link %}{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}{% endcapture -%}
|
{%- capture pg_link %}{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}{% endcapture -%}
|
||||||
<a href="{{ pg_link | relative_link }}" target="_blank">View Page</a>
|
<a href="{{ pg_link | relative_link }}" target="_blank">View Page</a>
|
||||||
<span class="text-muted"> • </span>
|
{% if is_editor or is_author and user_id == pg.author_id %}
|
||||||
<a href="{{ pg | edit_page_link }}">Edit</a>
|
<span class="text-muted"> • </span>
|
||||||
<span class="text-muted"> • </span>
|
<a href="{{ pg | edit_page_link }}">Edit</a>
|
||||||
{%- capture pg_del %}admin/page/{{ pg.id }}/delete{% endcapture -%}
|
{% endif %}
|
||||||
{%- capture pg_del_link %}{{ pg_del | relative_link }}{% endcapture -%}
|
{% if is_web_log_admin %}
|
||||||
<a href="{{ pg_del_link }}" hx-post="{{ pg_del_link }}" class="text-danger"
|
<span class="text-muted"> • </span>
|
||||||
hx-confirm="Are you sure you want to delete the page “{{ pg.title | strip_html | escape }}”? This action cannot be undone.">
|
{%- assign pg_del_link = "admin/page/" | append: pg.id | append: "/delete" | relative_link -%}
|
||||||
Delete
|
<a href="{{ pg_del_link }}" hx-post="{{ pg_del_link }}" class="text-danger"
|
||||||
</a>
|
hx-confirm="Are you sure you want to delete the page “{{ pg.title | strip_html | escape }}”? This action cannot be undone.">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="{{ link_col }}">
|
<div class="{{ link_col }}">
|
||||||
|
@ -55,14 +58,18 @@
|
||||||
<div class="d-flex justify-content-evenly pb-3">
|
<div class="d-flex justify-content-evenly pb-3">
|
||||||
<div>
|
<div>
|
||||||
{% if page_nbr > 1 %}
|
{% if page_nbr > 1 %}
|
||||||
{%- capture prev_link %}admin/pages{{ prev_page }}{% endcapture -%}
|
<p>
|
||||||
<p><a class="btn btn-default" href="{{ prev_link | relative_link }}">« Previous</a></p>
|
<a class="btn btn-default" href="{{ "admin/pages" | append: prev_page | relative_link }}">
|
||||||
|
« Previous
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
{% if page_count == 25 %}
|
{% if page_count == 25 %}
|
||||||
{%- capture next_link %}admin/pages{{ next_page }}{% endcapture -%}
|
<p>
|
||||||
<p><a class="btn btn-default" href="{{ next_link | relative_link }}">Next »</a></p>
|
<a class="btn btn-default" href="{{ "admin/pages" | append: next_page | relative_link }}">Next »</a>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<h2 class="my-3">{{ page_title }}</h2>
|
<h2 class="my-3">{{ page_title }}</h2>
|
||||||
<article>
|
<article>
|
||||||
{%- capture form_action %}admin/{{ model.entity }}/permalinks{% endcapture -%}
|
{%- assign base_url = "admin/" | append: model.entity | append: "/" -%}
|
||||||
<form action="{{ form_action | relative_link }}" method="post">
|
<form action="{{ base_url | append: "permalinks" | relative_link }}" method="post">
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
<input type="hidden" name="id" value="{{ model.id }}">
|
<input type="hidden" name="id" value="{{ model.id }}">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -11,8 +11,9 @@
|
||||||
<strong>{{ model.current_title }}</strong><br>
|
<strong>{{ model.current_title }}</strong><br>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<span class="fst-italic">{{ model.current_permalink }}</span><br>
|
<span class="fst-italic">{{ model.current_permalink }}</span><br>
|
||||||
{%- capture back_link %}admin/{{ model.entity }}/{{ model.id }}/edit{% endcapture -%}
|
<a href="{{ base_url | append: model.id | append: "/edit" | relative_link }}">
|
||||||
<a href="{{ back_link | relative_link }}">« Back to Edit {{ model.entity | capitalize }}</a>
|
« Back to Edit {{ model.entity | capitalize }}
|
||||||
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,15 +46,18 @@
|
||||||
{{ post.title }}<br>
|
{{ post.title }}<br>
|
||||||
<small>
|
<small>
|
||||||
<a href="{{ post | relative_link }}" target="_blank">View Post</a>
|
<a href="{{ post | relative_link }}" target="_blank">View Post</a>
|
||||||
<span class="text-muted"> • </span>
|
{% if is_editor or is_author and user_id == post.author_id %}
|
||||||
<a href="{{ post | edit_post_link }}">Edit</a>
|
<span class="text-muted"> • </span>
|
||||||
<span class="text-muted"> • </span>
|
<a href="{{ post | edit_post_link }}">Edit</a>
|
||||||
{%- capture post_del %}admin/post/{{ post.id }}/delete{% endcapture -%}
|
{% endif %}
|
||||||
{%- capture post_del_link %}{{ post_del | relative_link }}{% endcapture -%}
|
{% if is_web_log_admin %}
|
||||||
<a href="{{ post_del_link }}" hx-post="{{ post_del_link }}" class="text-danger"
|
<span class="text-muted"> • </span>
|
||||||
hx-confirm="Are you sure you want to delete the page “{{ post.title | strip_html | escape }}”? This action cannot be undone.">
|
{%- assign post_del_link = "admin/post/" | append: post.id | append: "/delete" | relative_link -%}
|
||||||
Delete
|
<a href="{{ post_del_link }}" hx-post="{{ post_del_link }}" class="text-danger"
|
||||||
</a>
|
hx-confirm="Are you sure you want to delete the page “{{ post.title | strip_html | escape }}”? This action cannot be undone.">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="{{ author_col }}">
|
<div class="{{ author_col }}">
|
||||||
|
|
|
@ -85,13 +85,12 @@
|
||||||
{{ feed.source }}
|
{{ feed.source }}
|
||||||
{%- if feed.is_podcast %} <span class="badge bg-primary">PODCAST</span>{% endif %}<br>
|
{%- if feed.is_podcast %} <span class="badge bg-primary">PODCAST</span>{% endif %}<br>
|
||||||
<small>
|
<small>
|
||||||
|
{%- assign feed_url = "admin/settings/rss/" | append: feed.id -%}
|
||||||
<a href="{{ feed.path | relative_link }}" target="_blank">View Feed</a>
|
<a href="{{ feed.path | relative_link }}" target="_blank">View Feed</a>
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- capture feed_edit %}admin/settings/rss/{{ feed.id }}/edit{% endcapture -%}
|
<a href="{{ feed_url | append: "/edit" | relative_link }}">Edit</a>
|
||||||
<a href="{{ feed_edit | relative_link }}">Edit</a>
|
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- capture feed_del %}admin/settings/rss/{{ feed.id }}/delete{% endcapture -%}
|
{%- assign feed_del_link = feed_url | append: "/delete" | relative_link -%}
|
||||||
{%- capture feed_del_link %}{{ feed_del | relative_link }}{% endcapture -%}
|
|
||||||
<a href="{{ feed_del_link }}" hx-post="{{ feed_del_link }}" class="text-danger"
|
<a href="{{ feed_del_link }}" hx-post="{{ feed_del_link }}" class="text-danger"
|
||||||
hx-confirm="Are you sure you want to delete the custom RSS feed based on {{ feed.source | strip_html | escape }}? This action cannot be undone.">
|
hx-confirm="Are you sure you want to delete the custom RSS feed based on {{ feed.source | strip_html | escape }}? This action cannot be undone.">
|
||||||
Delete
|
Delete
|
||||||
|
|
|
@ -9,14 +9,13 @@
|
||||||
<div class="col no-wrap">
|
<div class="col no-wrap">
|
||||||
{{ map.tag }}<br>
|
{{ map.tag }}<br>
|
||||||
<small>
|
<small>
|
||||||
{%- capture map_edit %}admin/settings/tag-mapping/{{ map_id }}/edit{% endcapture -%}
|
{%- assign map_url = "admin/settings/tag-mapping/" | append: map_id -%}
|
||||||
<a href="{{ map_edit | relative_link }}" hx-target="#tag_{{ map_id }}"
|
<a href="{{ map_url | append: "/edit" | relative_link }}" hx-target="#tag_{{ map_id }}"
|
||||||
hx-swap="innerHTML show:#tag_{{ map_id }}:top">
|
hx-swap="innerHTML show:#tag_{{ map_id }}:top">
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- capture map_del %}admin/settings/tag-mapping/{{ map_id }}/delete{% endcapture -%}
|
{%- assign map_del_link = map_url | append: "/delete" | relative_link -%}
|
||||||
{%- capture map_del_link %}{{ map_del | relative_link }}{% endcapture -%}
|
|
||||||
<a href="{{ map_del_link }}" hx-post="{{ map_del_link }}" class="text-danger"
|
<a href="{{ map_del_link }}" hx-post="{{ map_del_link }}" class="text-danger"
|
||||||
hx-confirm="Are you sure you want to delete the mapping for “{{ map.tag }}”? This action cannot be undone.">
|
hx-confirm="Are you sure you want to delete the mapping for “{{ map.tag }}”? This action cannot be undone.">
|
||||||
Delete
|
Delete
|
||||||
|
|
|
@ -22,12 +22,12 @@
|
||||||
{%- capture badge_class -%}
|
{%- capture badge_class -%}
|
||||||
{%- if file.source == "disk" %}secondary{% else %}primary{% endif -%}
|
{%- if file.source == "disk" %}secondary{% else %}primary{% endif -%}
|
||||||
{%- endcapture -%}
|
{%- endcapture -%}
|
||||||
{%- capture rel_url %}{{ upload_base }}{{ file.path }}{{ file.name }}{% endcapture -%}
|
{%- assign path_and_name = file.path | append: file.name -%}
|
||||||
{%- capture blog_rel %}{{ upload_path }}{{ file.path }}{{ file.name }}{% endcapture -%}
|
{%- assign blog_rel = upload_path | append: path_and_name -%}
|
||||||
<span class="badge bg-{{ badge_class }} text-uppercase float-end mt-1">{{ file.source }}</span>
|
<span class="badge bg-{{ badge_class }} text-uppercase float-end mt-1">{{ file.source }}</span>
|
||||||
{{ file.name }}<br>
|
{{ file.name }}<br>
|
||||||
<small>
|
<small>
|
||||||
<a href="{{ rel_url }}" target="_blank">View File</a>
|
<a href="{{ upload_base | append: path_and_name }}" target="_blank">View File</a>
|
||||||
<span class="text-muted"> • Copy </span>
|
<span class="text-muted"> • Copy </span>
|
||||||
<a href="{{ blog_rel | absolute_link }}" hx-boost="false"
|
<a href="{{ blog_rel | absolute_link }}" hx-boost="false"
|
||||||
onclick="return Admin.copyText('{{ blog_rel | absolute_link }}', this)">
|
onclick="return Admin.copyText('{{ blog_rel | absolute_link }}', this)">
|
||||||
|
@ -45,17 +45,20 @@
|
||||||
For Post
|
For Post
|
||||||
</a>
|
</a>
|
||||||
{%- endunless %}
|
{%- endunless %}
|
||||||
<span class="text-muted"> Link • </span>
|
<span class="text-muted"> Link</span>
|
||||||
{%- capture delete_url -%}
|
{% if is_web_log_admin %}
|
||||||
{%- if file.source == "disk" -%}
|
<span class="text-muted"> • </span>
|
||||||
admin/upload/delete/{{ file.path }}{{ file.name }}
|
{%- capture delete_url -%}
|
||||||
{%- else -%}
|
{%- if file.source == "disk" -%}
|
||||||
admin/upload/{{ file.id }}/delete
|
admin/upload/delete/{{ path_and_name }}
|
||||||
{%- endif -%}
|
{%- else -%}
|
||||||
{%- endcapture -%}
|
admin/upload/{{ file.id }}/delete
|
||||||
<a href="{{ delete_url | relative_link }}" hx-post="{{ delete_url | relative_link }}"
|
{%- endif -%}
|
||||||
hx-confirm="Are you sure you want to delete {{ file.name }}? This action cannot be undone."
|
{%- endcapture -%}
|
||||||
class="text-danger">Delete</a>
|
<a href="{{ delete_url | relative_link }}" hx-post="{{ delete_url | relative_link }}"
|
||||||
|
hx-confirm="Are you sure you want to delete {{ file.name }}? This action cannot be undone."
|
||||||
|
class="text-danger">Delete</a>
|
||||||
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">{{ file.path }}</div>
|
<div class="col-3">{{ file.path }}</div>
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
myWebLog Admin
|
myWebLog Admin
|
||||||
2.0.0-beta03
|
2.0.0-beta05
|
|
@ -8,7 +8,9 @@
|
||||||
**DRAFT**
|
**DRAFT**
|
||||||
{% endif %}
|
{% endif %}
|
||||||
by {{ model.authors | value: post.author_id }}
|
by {{ model.authors | value: post.author_id }}
|
||||||
{% if logged_on %} • <a hx-boost="false" href="{{ post | edit_post_link }}">Edit Post</a> {% endif %}
|
{%- if is_editor or is_author and user_id == post.author_id %}
|
||||||
|
• <a hx-boost="false" href="{{ post | edit_post_link }}">Edit Post</a>
|
||||||
|
{%- endif %}
|
||||||
</h4>
|
</h4>
|
||||||
<div>
|
<div>
|
||||||
<article class="container mt-3">
|
<article class="container mt-3">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user