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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user