Switch to server-side auth
This commit is contained in:
parent
31d5eeb986
commit
c0c5709194
|
@ -6,18 +6,44 @@ module MyPrayerJournal.Handlers
|
|||
|
||||
open Giraffe
|
||||
open Giraffe.Htmx
|
||||
open Microsoft.AspNetCore.Authentication
|
||||
open Microsoft.AspNetCore.Http
|
||||
open System
|
||||
open System.Security.Claims
|
||||
|
||||
/// Helper function to be able to split out log on
|
||||
[<AutoOpen>]
|
||||
module private LogOnHelpers =
|
||||
|
||||
/// Log on, optionally specifying a redirected URL once authentication is complete
|
||||
let logOn url : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
match url with
|
||||
| Some it ->
|
||||
do! ctx.ChallengeAsync ("Auth0", AuthenticationProperties (RedirectUri = it))
|
||||
return! next ctx
|
||||
| None -> return! challenge "Auth0" next ctx
|
||||
}
|
||||
|
||||
/// Handlers for error conditions
|
||||
module Error =
|
||||
|
||||
open Microsoft.Extensions.Logging
|
||||
open System.Threading.Tasks
|
||||
|
||||
/// Handle errors
|
||||
let error (ex : Exception) (log : ILogger) =
|
||||
log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.")
|
||||
clearResponse >=> setStatusCode 500 >=> json ex.Message
|
||||
|
||||
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized reponse
|
||||
let notAuthorized : HttpHandler =
|
||||
fun next ctx ->
|
||||
(next, ctx)
|
||||
||> match ctx.Request.Method with
|
||||
| "GET" -> logOn None
|
||||
| _ -> setStatusCode 401 >=> fun _ _ -> Task.FromResult<HttpContext option> None
|
||||
|
||||
/// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there
|
||||
let notFound : HttpHandler =
|
||||
setStatusCode 404 >=> text "Not found"
|
||||
|
@ -28,36 +54,14 @@ module Error =
|
|||
module private Helpers =
|
||||
|
||||
open LiteDB
|
||||
open Microsoft.AspNetCore.Authentication
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.AspNetCore.Http.Features.Authentication
|
||||
open Microsoft.Extensions.Logging
|
||||
open Microsoft.Net.Http.Headers
|
||||
open System.Security.Claims
|
||||
open System.Threading.Tasks
|
||||
|
||||
let debug (ctx : HttpContext) message =
|
||||
let fac = ctx.GetService<ILoggerFactory>()
|
||||
let log = fac.CreateLogger "Debug"
|
||||
log.LogInformation message
|
||||
|
||||
/// This type is internal in ASP.NET Core. :(
|
||||
type AuthFeatures (result : AuthenticateResult) =
|
||||
let mutable _user : ClaimsPrincipal = match result with null -> null | r -> r.Principal
|
||||
let mutable _result : AuthenticateResult = result
|
||||
interface IAuthenticateResultFeature with
|
||||
member __.AuthenticateResult
|
||||
with get () = _result
|
||||
and set v =
|
||||
_result <- v
|
||||
_user <- match _result with null -> null | rslt -> rslt.Principal
|
||||
interface IHttpAuthenticationFeature with
|
||||
member __.User
|
||||
with get () = _user
|
||||
and set v =
|
||||
_user <- v
|
||||
_result <- null
|
||||
|
||||
/// Get the LiteDB database
|
||||
let db (ctx : HttpContext) = ctx.GetService<LiteDatabase>()
|
||||
|
||||
|
@ -67,11 +71,12 @@ module private Helpers =
|
|||
|> Option.ofObj
|
||||
|> Option.map (fun user -> user.Claims |> Seq.tryFind (fun u -> u.Type = ClaimTypes.NameIdentifier))
|
||||
|> Option.flatten
|
||||
|> Option.map (fun claim -> claim.Value)
|
||||
|
||||
/// Get the current user's ID
|
||||
// NOTE: this may raise if you don't run the request through the authorize handler first
|
||||
// NOTE: this may raise if you don't run the request through the requiresAuthentication handler first
|
||||
let userId ctx =
|
||||
((user >> Option.get) ctx).Value |> UserId
|
||||
(user >> Option.get) ctx |> UserId
|
||||
|
||||
/// Return a 201 CREATED response
|
||||
let created =
|
||||
|
@ -87,54 +92,6 @@ module private Helpers =
|
|||
let jsNow () =
|
||||
DateTime.UtcNow.Subtract(DateTime (1970, 1, 1, 0, 0, 0)).TotalSeconds |> (int64 >> ( * ) 1_000L >> Ticks)
|
||||
|
||||
/// Handler to return a 401 Not Authorized reponse
|
||||
let notAuthorized : HttpHandler =
|
||||
setStatusCode 401 >=> fun _ _ -> Task.FromResult<HttpContext option> None
|
||||
|
||||
/// Handler to require authorization
|
||||
// NOTE: This is cribbed from ASP.NET Core's `AuthenticationMiddleware#Invoke`
|
||||
// https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs
|
||||
let authorize : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
let schemes = ctx.GetService<IAuthenticationSchemeProvider> ()
|
||||
ctx.Features.Set<IAuthenticationFeature>
|
||||
(AuthenticationFeature (OriginalPath = ctx.Request.Path, OriginalPathBase = ctx.Request.PathBase))
|
||||
|
||||
// Give any IAuthenticationRequestHandler schemes a chance to handle the request
|
||||
let handlers = ctx.GetService<IAuthenticationHandlerProvider> ()
|
||||
let! schms = schemes.GetRequestHandlerSchemesAsync ()
|
||||
let mutable handled = false
|
||||
|
||||
for schm in schms do
|
||||
match handled with
|
||||
| true -> ()
|
||||
| false ->
|
||||
match! handlers.GetHandlerAsync (ctx, schm.Name) with
|
||||
| null -> ()
|
||||
| :? IAuthenticationRequestHandler as handler ->
|
||||
match! handler.HandleRequestAsync () with true -> handled <- true | _ -> ()
|
||||
| _ -> ()
|
||||
|
||||
match handled with
|
||||
| true -> return None
|
||||
| false ->
|
||||
match! schemes.GetDefaultAuthenticateSchemeAsync () with
|
||||
| null -> ()
|
||||
| auth ->
|
||||
match! ctx.AuthenticateAsync auth.Name with
|
||||
| null -> ()
|
||||
| result ->
|
||||
match result.Principal with null -> () | _ -> ctx.User <- result.Principal
|
||||
match result.Succeeded with
|
||||
| true ->
|
||||
let authFeatures = AuthFeatures result
|
||||
ctx.Features.Set<IHttpAuthenticationFeature> authFeatures
|
||||
ctx.Features.Set<IAuthenticateResultFeature> authFeatures
|
||||
| false -> ()
|
||||
|
||||
return! match user ctx with Some _ -> next ctx | None -> notAuthorized next ctx
|
||||
}
|
||||
|
||||
/// Render a component result
|
||||
let renderComponent nodes : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
|
@ -212,7 +169,7 @@ module Components =
|
|||
|
||||
// GET /components/journal-items
|
||||
let journalItems : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let shouldShow now r = now > Ticks.toLong r.snoozedUntil && now > Ticks.toLong r.showAfter
|
||||
let! jrnl = Data.journalByUserId (userId ctx) (db ctx)
|
||||
|
@ -222,7 +179,7 @@ module Components =
|
|||
|
||||
// GET /components/request/[req-id]/edit
|
||||
let requestEdit requestId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
match requestId with
|
||||
| "new" ->
|
||||
|
@ -230,22 +187,17 @@ module Components =
|
|||
(Views.Request.edit (JournalRequest.ofRequestLite Request.empty) false) next ctx
|
||||
| _ ->
|
||||
match! Data.tryJournalById (RequestId.ofString requestId) (userId ctx) (db ctx) with
|
||||
| Some req ->
|
||||
return! partialIfNotRefresh "Edit Prayer Request" (Views.Request.edit req false) next ctx
|
||||
| Some req -> return! partialIfNotRefresh "Edit Prayer Request" (Views.Request.edit req false) next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET /components/request-item/[req-id]
|
||||
let requestItem reqId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
match! Data.tryJournalById (RequestId.ofString reqId) (userId ctx) (db ctx) with
|
||||
| Some req ->
|
||||
debug ctx "Found the item"
|
||||
return! renderComponent [ Views.Request.reqListItem req ] next ctx
|
||||
| None ->
|
||||
debug ctx "Did not find the item"
|
||||
return! Error.notFound next ctx
|
||||
| Some req -> return! renderComponent [ Views.Request.reqListItem req ] next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
||||
|
@ -256,20 +208,20 @@ module Home =
|
|||
let home : HttpHandler =
|
||||
partialIfNotRefresh "Welcome!" Views.Home.home
|
||||
|
||||
// GET /user/log-on
|
||||
let logOn : HttpHandler =
|
||||
partialIfNotRefresh "Logging on..." Views.Home.logOn
|
||||
|
||||
|
||||
/// /journal URL
|
||||
module Journal =
|
||||
|
||||
// GET /journal
|
||||
let journal : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let usr = ctx.Request.Headers.["X-Given-Name"].[0]
|
||||
return! partialIfNotRefresh "Your Prayer Journal" (Views.Journal.journal usr) next ctx
|
||||
let usr =
|
||||
ctx.User.Claims
|
||||
|> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName)
|
||||
|> Option.map (fun c -> c.Value)
|
||||
|> Option.defaultValue "Your"
|
||||
return! partialIfNotRefresh (sprintf "%s Prayer Journal" usr) (Views.Journal.journal usr) next ctx
|
||||
}
|
||||
|
||||
|
||||
|
@ -294,7 +246,7 @@ module Request =
|
|||
|
||||
// PATCH /request/[req-id]/prayed
|
||||
let prayed requestId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let db = db ctx
|
||||
let usrId = userId ctx
|
||||
|
@ -315,7 +267,7 @@ module Request =
|
|||
|
||||
/// POST /api/request/[req-id]/note
|
||||
let addNote requestId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let db = db ctx
|
||||
let usrId = userId ctx
|
||||
|
@ -329,17 +281,17 @@ module Request =
|
|||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
/// GET /requests/active
|
||||
// GET /requests/active
|
||||
let active : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let! reqs = Data.journalByUserId (userId ctx) (db ctx)
|
||||
return! partialIfNotRefresh "Active Requests" (Views.Request.active reqs) next ctx
|
||||
}
|
||||
|
||||
/// GET /requests/snoozed
|
||||
// GET /requests/snoozed
|
||||
let snoozed : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let! reqs = Data.journalByUserId (userId ctx) (db ctx)
|
||||
let now = (jsNow >> Ticks.toLong) ()
|
||||
|
@ -347,9 +299,9 @@ module Request =
|
|||
return! partialIfNotRefresh "Active Requests" (Views.Request.snoozed snoozed) next ctx
|
||||
}
|
||||
|
||||
/// GET /requests/answered
|
||||
// GET /requests/answered
|
||||
let answered : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let! reqs = Data.answeredRequests (userId ctx) (db ctx)
|
||||
return! partialIfNotRefresh "Answered Requests" (Views.Request.answered reqs) next ctx
|
||||
|
@ -357,7 +309,7 @@ module Request =
|
|||
|
||||
/// GET /api/request/[req-id]
|
||||
let get requestId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
match! Data.tryJournalById (RequestId.ofString requestId) (userId ctx) (db ctx) with
|
||||
| Some req -> return! json req next ctx
|
||||
|
@ -366,7 +318,7 @@ module Request =
|
|||
|
||||
// GET /request/[req-id]/full
|
||||
let getFull requestId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
match! Data.tryFullRequestById (RequestId.ofString requestId) (userId ctx) (db ctx) with
|
||||
| Some req -> return! partialIfNotRefresh "Prayer Request" (Views.Request.full req) next ctx
|
||||
|
@ -375,7 +327,7 @@ module Request =
|
|||
|
||||
/// GET /api/request/[req-id]/notes
|
||||
let getNotes requestId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx)
|
||||
return! json notes next ctx
|
||||
|
@ -383,7 +335,7 @@ module Request =
|
|||
|
||||
// PATCH /request/[req-id]/show
|
||||
let show requestId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let db = db ctx
|
||||
let usrId = userId ctx
|
||||
|
@ -398,7 +350,7 @@ module Request =
|
|||
|
||||
/// PATCH /api/request/[req-id]/snooze
|
||||
let snooze requestId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let db = db ctx
|
||||
let usrId = userId ctx
|
||||
|
@ -414,7 +366,7 @@ module Request =
|
|||
|
||||
// PATCH /request/[req-id]/cancel-snooze
|
||||
let cancelSnooze requestId : HttpHandler =
|
||||
authorize
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let db = db ctx
|
||||
let usrId = userId ctx
|
||||
|
@ -434,7 +386,8 @@ module Request =
|
|||
|
||||
// POST /request
|
||||
let add : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
let! form = ctx.BindModelAsync<Models.Request> ()
|
||||
let db = db ctx
|
||||
let usrId = userId ctx
|
||||
|
@ -462,7 +415,8 @@ module Request =
|
|||
|
||||
// PATCH /request
|
||||
let update : HttpHandler =
|
||||
fun next ctx -> Ply.task {
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> Ply.task {
|
||||
let! form = ctx.BindModelAsync<Models.Request> ()
|
||||
let db = db ctx
|
||||
let usrId = userId ctx
|
||||
|
@ -489,6 +443,25 @@ module Request =
|
|||
}
|
||||
|
||||
|
||||
/// Handlers for /user URLs
|
||||
module User =
|
||||
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
|
||||
// GET /user/log-on
|
||||
let logOn : HttpHandler =
|
||||
logOn (Some "/journal")
|
||||
|
||||
// GET /user/log-off
|
||||
let logOff : HttpHandler =
|
||||
requiresAuthentication Error.notAuthorized
|
||||
>=> fun next ctx -> task {
|
||||
do! ctx.SignOutAsync ("Auth0", AuthenticationProperties (RedirectUri = "/"))
|
||||
do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
|
||||
return! next ctx
|
||||
}
|
||||
|
||||
|
||||
open Giraffe.EndpointRouting
|
||||
|
||||
/// The routes for myPrayerJournal
|
||||
|
@ -526,7 +499,12 @@ let routes =
|
|||
route "" Request.add
|
||||
]
|
||||
]
|
||||
GET_HEAD [ route "/user/log-on" Home.logOn ]
|
||||
subRoute "/user/" [
|
||||
GET_HEAD [
|
||||
route "log-off" User.logOff
|
||||
route "log-on" User.logOn
|
||||
]
|
||||
]
|
||||
subRoute "/api/" [
|
||||
GET [
|
||||
subRoute "request" [
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<PackageReference Include="FunctionalCuid" Version="1.0.0" />
|
||||
<PackageReference Include="Giraffe" Version="5.0.0" />
|
||||
<PackageReference Include="LiteDB" Version="5.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.10" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../../Giraffe.Htmx/src/Htmx/Giraffe.Htmx.fsproj" />
|
||||
|
|
|
@ -56,27 +56,75 @@ module Configure =
|
|||
|
||||
open Giraffe
|
||||
open LiteDB
|
||||
open Microsoft.AspNetCore.Authentication.JwtBearer
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
open Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Microsoft.IdentityModel.Protocols.OpenIdConnect
|
||||
open System
|
||||
open System.Text.Json
|
||||
open System.Text.Json.Serialization
|
||||
open System.Threading.Tasks
|
||||
|
||||
/// Configure dependency injection
|
||||
let services (bldr : WebApplicationBuilder) =
|
||||
let sameSite (opts : CookieOptions) =
|
||||
match opts.SameSite, opts.Secure with
|
||||
| SameSiteMode.None, false -> opts.SameSite <- SameSiteMode.Unspecified
|
||||
| _, _ -> ()
|
||||
|
||||
bldr.Services
|
||||
.AddRouting()
|
||||
.AddGiraffe()
|
||||
.Configure<CookiePolicyOptions>(
|
||||
fun (opts : CookiePolicyOptions) ->
|
||||
opts.MinimumSameSitePolicy <- SameSiteMode.Unspecified
|
||||
opts.OnAppendCookie <- fun ctx -> sameSite ctx.CookieOptions
|
||||
opts.OnDeleteCookie <- fun ctx -> sameSite ctx.CookieOptions)
|
||||
.AddAuthentication(
|
||||
/// Use HTTP "Bearer" authentication with JWTs
|
||||
fun opts ->
|
||||
opts.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme
|
||||
opts.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(
|
||||
/// Configure JWT options with Auth0 options from configuration
|
||||
opts.DefaultAuthenticateScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
||||
opts.DefaultSignInScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
||||
opts.DefaultChallengeScheme <- CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie()
|
||||
.AddOpenIdConnect("Auth0",
|
||||
/// Configure OIDC with Auth0 options from configuration
|
||||
fun opts ->
|
||||
let jwtCfg = bldr.Configuration.GetSection "Auth0"
|
||||
opts.Authority <- sprintf "https://%s/" jwtCfg.["Domain"]
|
||||
opts.Audience <- jwtCfg.["Audience"])
|
||||
let cfg = bldr.Configuration.GetSection "Auth0"
|
||||
opts.Authority <- sprintf "https://%s/" cfg.["Domain"]
|
||||
opts.ClientId <- cfg.["Id"]
|
||||
opts.ClientSecret <- cfg.["Secret"]
|
||||
opts.ResponseType <- OpenIdConnectResponseType.Code
|
||||
|
||||
opts.Scope.Clear ()
|
||||
opts.Scope.Add "openid"
|
||||
opts.Scope.Add "profile"
|
||||
|
||||
opts.CallbackPath <- PathString "/user/log-on/success"
|
||||
opts.ClaimsIssuer <- "Auth0"
|
||||
opts.SaveTokens <- true
|
||||
|
||||
opts.Events <- OpenIdConnectEvents ()
|
||||
opts.Events.OnRedirectToIdentityProviderForSignOut <- fun ctx ->
|
||||
let returnTo =
|
||||
match ctx.Properties.RedirectUri with
|
||||
| it when isNull it || it = "" -> ""
|
||||
| redirUri ->
|
||||
let finalRedirUri =
|
||||
match redirUri.StartsWith "/" with
|
||||
| true ->
|
||||
// transform to absolute
|
||||
let request = ctx.Request
|
||||
sprintf "%s://%s%s%s" request.Scheme request.Host.Value request.PathBase.Value redirUri
|
||||
| false -> redirUri
|
||||
Uri.EscapeDataString finalRedirUri |> sprintf "&returnTo=%s"
|
||||
sprintf "https://%s/v2/logout?client_id=%s%s" cfg.["Domain"] cfg.["Id"] returnTo
|
||||
|> ctx.Response.Redirect
|
||||
ctx.HandleResponse ()
|
||||
|
||||
Task.CompletedTask
|
||||
)
|
||||
|> ignore
|
||||
let jsonOptions = JsonSerializerOptions ()
|
||||
jsonOptions.Converters.Add (JsonFSharpConverter ())
|
||||
|
@ -97,11 +145,14 @@ module Configure =
|
|||
| true -> app.UseDeveloperExceptionPage ()
|
||||
| false -> app.UseGiraffeErrorHandler Handlers.Error.error
|
||||
|> ignore
|
||||
app.UseAuthentication()
|
||||
.UseStaticFiles()
|
||||
app.UseStaticFiles()
|
||||
.UseCookiePolicy()
|
||||
.UseRouting()
|
||||
.UseAuthentication()
|
||||
// .UseAuthorization()
|
||||
.UseEndpoints (fun e ->
|
||||
e.MapGiraffeEndpoints Handlers.routes
|
||||
// TODO: fallback to 404
|
||||
e.MapFallbackToFile "index.html" |> ignore)
|
||||
|> ignore
|
||||
app
|
||||
|
|
|
@ -9,7 +9,10 @@ open System
|
|||
module Helpers =
|
||||
|
||||
/// Create a link that targets the `main` element and pushes a URL to history
|
||||
let pageLink href attrs = a (attrs |> List.append [ _href href; _hxBoost; _hxTarget "main"; _hxPushUrl ])
|
||||
let pageLink href attrs =
|
||||
attrs
|
||||
|> List.append [ _href href; _hxBoost; _hxTarget "main"; _hxPushUrl ]
|
||||
|> a
|
||||
|
||||
/// Create a Material icon
|
||||
let icon name = span [ _class "material-icons" ] [ str name ]
|
||||
|
@ -235,18 +238,18 @@ module Navigation =
|
|||
/// Generate the navigation items based on the current state
|
||||
let currentNav isAuthenticated hasSnoozed (url : Uri option) =
|
||||
seq {
|
||||
let currUrl = match url with Some u -> (u.PathAndQuery.Split '?').[0] | None -> ""
|
||||
let navLink (matchUrl : string) =
|
||||
match currUrl.StartsWith matchUrl with true -> [ _class "is-active-route" ] | false -> []
|
||||
|> pageLink matchUrl
|
||||
match isAuthenticated with
|
||||
| true ->
|
||||
let currUrl = match url with Some u -> (u.PathAndQuery.Split '?').[0] | None -> ""
|
||||
let navLink (matchUrl : string) =
|
||||
match currUrl.StartsWith matchUrl with true -> [ _class "is-active-route" ] | false -> []
|
||||
|> pageLink matchUrl
|
||||
li [ _class "nav-item" ] [ navLink "/journal" [ str "Journal" ] ]
|
||||
li [ _class "nav-item" ] [ navLink "/requests/active" [ str "Active" ] ]
|
||||
if hasSnoozed then li [ _class "nav-item" ] [ navLink "/requests/snoozed" [ str "Snoozed" ] ]
|
||||
li [ _class "nav-item" ] [ navLink "/requests/answered" [ str "Answered" ] ]
|
||||
li [ _class "nav-item" ] [ a [ _href "/user/log-off"; _onclick "mpj.logOff(event)" ] [ str "Log Off" ] ]
|
||||
| false -> li [ _class "nav-item"] [ a [ _href "/user/log-on"; _onclick "mpj.logOn(event)"] [ str "Log On" ] ]
|
||||
li [ _class "nav-item" ] [ a [ _href "/user/log-off" ] [ str "Log Off" ] ]
|
||||
| false -> li [ _class "nav-item"] [ a [ _href "/user/log-on" ] [ str "Log On" ] ]
|
||||
li [ _class "nav-item" ] [ a [ _href "https://docs.prayerjournal.me"; _target "_blank" ] [ str "Docs" ] ]
|
||||
}
|
||||
|> List.ofSeq
|
||||
|
@ -290,7 +293,13 @@ module Journal =
|
|||
|
||||
/// The journal loading page
|
||||
let journal user = article [ _class "container-fluid mt-3" ] [
|
||||
h2 [ _class "pb-3" ] [ str user; rawText "’s Prayer Journal" ]
|
||||
h2 [ _class "pb-3" ] [
|
||||
str user
|
||||
match user with
|
||||
| "Your" -> ()
|
||||
| _ -> rawText "’s"
|
||||
str " Prayer Journal"
|
||||
]
|
||||
p [
|
||||
_hxGet "/components/journal-items"
|
||||
_hxSwap HxSwap.OuterHtml
|
||||
|
@ -571,7 +580,8 @@ module Layout =
|
|||
|
||||
/// The HTML `head` element
|
||||
let htmlHead pageTitle =
|
||||
head [] [
|
||||
head [ _lang "en" ] [
|
||||
meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ]
|
||||
title [] [ str pageTitle; rawText " « myPrayerJournal" ]
|
||||
link [
|
||||
_href "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||
|
@ -581,11 +591,6 @@ module Layout =
|
|||
]
|
||||
link [ _href "https://fonts.googleapis.com/icon?family=Material+Icons"; _rel "stylesheet" ]
|
||||
link [ _href "/style/style.css"; _rel "stylesheet" ]
|
||||
script [
|
||||
_src "https://unpkg.com/htmx.org@1.5.0"
|
||||
_integrity "sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI"
|
||||
_crossorigin "anonymous"
|
||||
] []
|
||||
]
|
||||
|
||||
/// Element used to display toasts
|
||||
|
@ -612,13 +617,17 @@ module Layout =
|
|||
]
|
||||
]
|
||||
]
|
||||
script [
|
||||
_src "https://unpkg.com/htmx.org@1.5.0"
|
||||
_integrity "sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI"
|
||||
_crossorigin "anonymous"
|
||||
] []
|
||||
script [
|
||||
_async
|
||||
_src "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
||||
_integrity "sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
||||
_crossorigin "anonymous"
|
||||
] []
|
||||
script [ _src "https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js" ] []
|
||||
script [ _src "/script/mpj.js" ] []
|
||||
]
|
||||
|
||||
|
|
|
@ -2,41 +2,6 @@
|
|||
|
||||
/** myPrayerJournal script */
|
||||
const mpj = {
|
||||
/** Auth0 configuration */
|
||||
auth: {
|
||||
/** The Auth0 client */
|
||||
auth0: null,
|
||||
/** Configure the Auth0 client */
|
||||
async configureClient () {
|
||||
const response = await fetch("/auth-config.json")
|
||||
const config = await response.json()
|
||||
mpj.auth.auth0 = await createAuth0Client({
|
||||
domain: config.domain,
|
||||
client_id: config.clientId,
|
||||
audience: config.audience
|
||||
})
|
||||
}
|
||||
},
|
||||
/** Whether the user is currently authenticated */
|
||||
isAuthenticated: false,
|
||||
/** Whether we should redirect to the journal the next time the menu items are refreshed */
|
||||
redirToJournal: false,
|
||||
/**
|
||||
* Process a log on request
|
||||
* @param {Event} e The HTML event from the `onclick` event
|
||||
*/
|
||||
async logOn (e) {
|
||||
e.preventDefault()
|
||||
await mpj.auth.auth0.loginWithRedirect({ redirect_uri: `${window.location.origin}/user/log-on` })
|
||||
},
|
||||
/**
|
||||
* Log the user off
|
||||
* @param {Event} e The HTML event from the `onclick` event
|
||||
*/
|
||||
logOff (e) {
|
||||
e.preventDefault()
|
||||
mpj.auth.auth0.logout({ returnTo: window.location.origin })
|
||||
},
|
||||
/**
|
||||
* Show a message via toast
|
||||
* @param {string} message The message to show
|
||||
|
@ -89,37 +54,6 @@ const mpj = {
|
|||
},
|
||||
}
|
||||
|
||||
window.onload = async () => {
|
||||
/** If the user is authenticated, set the JWT on the `body` tag */
|
||||
const establishAuth = async () => {
|
||||
mpj.isAuthenticated = await mpj.auth.auth0.isAuthenticated()
|
||||
if (mpj.isAuthenticated) {
|
||||
const token = await mpj.auth.auth0.getTokenSilently()
|
||||
const user = await mpj.auth.auth0.getUser()
|
||||
document.querySelector("body")
|
||||
.setAttribute("hx-headers", `{ "Authorization": "Bearer ${token}", "X-Given-Name": "${user.given_name}" }`)
|
||||
htmx.trigger(htmx.find(".navbar-nav"), "menu-refresh")
|
||||
}
|
||||
}
|
||||
|
||||
// Set up Auth0
|
||||
await mpj.auth.configureClient()
|
||||
await establishAuth()
|
||||
if (mpj.isAuthenticated) return
|
||||
|
||||
// Handle log on code, if present
|
||||
const query = window.location.search
|
||||
if (query.includes("code=") && query.includes("state=")) {
|
||||
await mpj.auth.auth0.handleRedirectCallback()
|
||||
await establishAuth()
|
||||
if (window.location.pathname === "/user/log-on") {
|
||||
mpj.redirToJournal = true
|
||||
} else {
|
||||
window.history.replaceState({}, document.title, "/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
htmx.on("htmx:afterOnLoad", function (evt) {
|
||||
const hdrs = evt.detail.xhr.getAllResponseHeaders()
|
||||
// Set the page title if a header was in the response
|
||||
|
@ -133,11 +67,3 @@ htmx.on("htmx:afterOnLoad", function (evt) {
|
|||
mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
|
||||
}
|
||||
})
|
||||
htmx.on("htmx:afterSettle", function (evt) {
|
||||
// Redirect to the journal (once menu items load after log on)
|
||||
if (mpj.redirToJournal
|
||||
&& ([...evt.target.attributes].find(it => it.name === "hx-target")?.value ?? "") === ".navbar-nav") {
|
||||
mpj.redirToJournal = false
|
||||
document.querySelector(`a[href="/journal"]`).click()
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue
Block a user