Finish recurrent model change
Also fix IDE warnings, update to htmx 1.6.1
This commit is contained in:
1 parent
d153a29b7e
commit
ed4bb64ca5
9 files changed
+67
-122
No files matched your search
@@ -1,8 +1,8 @@
|
|||||||
module MyPrayerJournal.Data
|
module MyPrayerJournal.Data
|
||||||
|
|
||||||
open LiteDB
|
open LiteDB
|
||||||
|
open MyPrayerJournal
|
||||||
open NodaTime
|
open NodaTime
|
||||||
open System
|
|
||||||
open System.Threading.Tasks
|
open System.Threading.Tasks
|
||||||
|
|
||||||
// fsharplint:disable MemberNames
|
// fsharplint:disable MemberNames
|
||||||
@@ -37,6 +37,11 @@ module Mapping =
|
|||||||
let stringFromBson (value : BsonValue) = match value.AsString with "" -> None | x -> Some x
|
let stringFromBson (value : BsonValue) = match value.AsString with "" -> None | x -> Some x
|
||||||
let stringToBson (value : string option) : BsonValue = match value with Some txt -> txt | None -> ""
|
let stringToBson (value : string option) : BsonValue = match value with Some txt -> txt | None -> ""
|
||||||
|
|
||||||
|
/// Mapping for Recurrence
|
||||||
|
module Recurrence =
|
||||||
|
let fromBson (value : BsonValue) = Recurrence.ofString value
|
||||||
|
let toBson (value : Recurrence) : BsonValue = Recurrence.toString value
|
||||||
|
|
||||||
/// Mapping for RequestAction
|
/// Mapping for RequestAction
|
||||||
module RequestAction =
|
module RequestAction =
|
||||||
let fromBson (value : BsonValue) = RequestAction.ofString value.AsString
|
let fromBson (value : BsonValue) = RequestAction.ofString value.AsString
|
||||||
@@ -52,65 +57,10 @@ module Mapping =
|
|||||||
let fromBson (value : BsonValue) = UserId value.AsString
|
let fromBson (value : BsonValue) = UserId value.AsString
|
||||||
let toBson (value : UserId) : BsonValue = UserId.toString value
|
let toBson (value : UserId) : BsonValue = UserId.toString value
|
||||||
|
|
||||||
/// Map a history entry to BSON
|
|
||||||
let historyToBson (hist : History) : BsonValue =
|
|
||||||
let doc = BsonDocument ()
|
|
||||||
doc["asOf"] <- hist.asOf.ToUnixTimeMilliseconds ()
|
|
||||||
doc["status"] <- RequestAction.toString hist.status
|
|
||||||
doc["text"] <- match hist.text with Some t -> t | None -> ""
|
|
||||||
upcast doc
|
|
||||||
|
|
||||||
/// Map a BSON document to a history entry
|
|
||||||
let historyFromBson (doc : BsonValue) =
|
|
||||||
{ asOf = Instant.FromUnixTimeMilliseconds doc["asOf"].AsInt64
|
|
||||||
status = RequestAction.ofString doc["status"].AsString
|
|
||||||
text = match doc["text"].AsString with "" -> None | txt -> Some txt
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map a note entry to BSON
|
|
||||||
let noteToBson (note : Note) : BsonValue =
|
|
||||||
let doc = BsonDocument ()
|
|
||||||
doc["asOf"] <- note.asOf.ToUnixTimeMilliseconds ()
|
|
||||||
doc["notes"] <- note.notes
|
|
||||||
upcast doc
|
|
||||||
|
|
||||||
/// Map a BSON document to a note entry
|
|
||||||
let noteFromBson (doc : BsonValue) =
|
|
||||||
{ asOf = Instant.FromUnixTimeMilliseconds doc["asOf"].AsInt64
|
|
||||||
notes = doc["notes"].AsString
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map a request to its BSON representation
|
|
||||||
let requestToBson req : BsonValue =
|
|
||||||
let doc = BsonDocument ()
|
|
||||||
doc["_id"] <- RequestId.toString req.id
|
|
||||||
doc["enteredOn"] <- req.enteredOn.ToUnixTimeMilliseconds ()
|
|
||||||
doc["userId"] <- UserId.toString req.userId
|
|
||||||
doc["snoozedUntil"] <- req.snoozedUntil.ToUnixTimeMilliseconds ()
|
|
||||||
doc["showAfter"] <- req.showAfter.ToUnixTimeMilliseconds ()
|
|
||||||
doc["recurType"] <- Recurrence.toString req.recurType
|
|
||||||
doc["recurCount"] <- BsonValue req.recurCount
|
|
||||||
doc["history"] <- BsonArray (req.history |> List.map historyToBson |> Seq.ofList)
|
|
||||||
doc["notes"] <- BsonArray (req.notes |> List.map noteToBson |> Seq.ofList)
|
|
||||||
upcast doc
|
|
||||||
|
|
||||||
/// Map a BSON document to a request
|
|
||||||
let requestFromBson (doc : BsonValue) =
|
|
||||||
{ id = RequestId.ofString doc["_id"].AsString
|
|
||||||
enteredOn = Instant.FromUnixTimeMilliseconds doc["enteredOn"].AsInt64
|
|
||||||
userId = UserId doc["userId"].AsString
|
|
||||||
snoozedUntil = Instant.FromUnixTimeMilliseconds doc["snoozedUntil"].AsInt64
|
|
||||||
showAfter = Instant.FromUnixTimeMilliseconds doc["showAfter"].AsInt64
|
|
||||||
recurType = Recurrence.ofString doc["recurType"].AsString
|
|
||||||
recurCount = int16 doc["recurCount"].AsInt32
|
|
||||||
history = doc["history"].AsArray |> Seq.map historyFromBson |> List.ofSeq
|
|
||||||
notes = doc["notes"].AsArray |> Seq.map noteFromBson |> List.ofSeq
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set up the mapping
|
/// Set up the mapping
|
||||||
let register () =
|
let register () =
|
||||||
BsonMapper.Global.RegisterType<Request>(requestToBson, requestFromBson)
|
|
||||||
BsonMapper.Global.RegisterType<Instant>(Instant.toBson, Instant.fromBson)
|
BsonMapper.Global.RegisterType<Instant>(Instant.toBson, Instant.fromBson)
|
||||||
|
BsonMapper.Global.RegisterType<Recurrence>(Recurrence.toBson, Recurrence.fromBson)
|
||||||
BsonMapper.Global.RegisterType<RequestAction>(RequestAction.toBson, RequestAction.fromBson)
|
BsonMapper.Global.RegisterType<RequestAction>(RequestAction.toBson, RequestAction.fromBson)
|
||||||
BsonMapper.Global.RegisterType<RequestId>(RequestId.toBson, RequestId.fromBson)
|
BsonMapper.Global.RegisterType<RequestId>(RequestId.toBson, RequestId.fromBson)
|
||||||
BsonMapper.Global.RegisterType<string option>(Option.stringToBson, Option.stringFromBson)
|
BsonMapper.Global.RegisterType<string option>(Option.stringToBson, Option.stringFromBson)
|
||||||
@@ -217,9 +167,9 @@ let tryJournalById reqId userId (db : LiteDatabase) = backgroundTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update the recurrence for a request
|
/// Update the recurrence for a request
|
||||||
let updateRecurrence reqId userId recurType recurCount db = backgroundTask {
|
let updateRecurrence reqId userId recurType db = backgroundTask {
|
||||||
match! tryFullRequestById reqId userId db with
|
match! tryFullRequestById reqId userId db with
|
||||||
| Some req -> do! doUpdate db { req with recurType = recurType; recurCount = recurCount }
|
| Some req -> do! doUpdate db { req with recurrence = recurType }
|
||||||
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
| None -> invalidOp $"{RequestId.toString reqId} not found"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[<AutoOpen>]
|
/// The data model for myPrayerJournal
|
||||||
/// The data model for myPrayerJournal
|
[<AutoOpen>]
|
||||||
module MyPrayerJournal.Domain
|
module MyPrayerJournal.Domain
|
||||||
|
|
||||||
// fsharplint:disable RecordFieldNames
|
// fsharplint:disable RecordFieldNames
|
||||||
@@ -34,7 +34,7 @@ module UserId =
|
|||||||
type Recurrence =
|
type Recurrence =
|
||||||
| Immediate
|
| Immediate
|
||||||
| Hours of int16
|
| Hours of int16
|
||||||
| Days of int16
|
| Days of int16
|
||||||
| Weeks of int16
|
| Weeks of int16
|
||||||
|
|
||||||
/// Functions to manipulate recurrences
|
/// Functions to manipulate recurrences
|
||||||
@@ -111,10 +111,8 @@ type Request = {
|
|||||||
snoozedUntil : Instant
|
snoozedUntil : Instant
|
||||||
/// The time at which this request should reappear in the user's journal by recurrence
|
/// The time at which this request should reappear in the user's journal by recurrence
|
||||||
showAfter : Instant
|
showAfter : Instant
|
||||||
/// The type of recurrence for this request
|
/// The recurrence for this request
|
||||||
recurType : Recurrence
|
recurrence : Recurrence
|
||||||
/// How many of the recurrence intervals should occur between appearances in the journal
|
|
||||||
recurCount : int16
|
|
||||||
/// The history entries for this request
|
/// The history entries for this request
|
||||||
history : History list
|
history : History list
|
||||||
/// The notes for this request
|
/// The notes for this request
|
||||||
@@ -128,8 +126,7 @@ with
|
|||||||
userId = UserId ""
|
userId = UserId ""
|
||||||
snoozedUntil = Instant.MinValue
|
snoozedUntil = Instant.MinValue
|
||||||
showAfter = Instant.MinValue
|
showAfter = Instant.MinValue
|
||||||
recurType = Immediate
|
recurrence = Immediate
|
||||||
recurCount = 0s
|
|
||||||
history = []
|
history = []
|
||||||
notes = []
|
notes = []
|
||||||
}
|
}
|
||||||
@@ -152,10 +149,8 @@ type JournalRequest = {
|
|||||||
snoozedUntil : Instant
|
snoozedUntil : Instant
|
||||||
/// The time after which this request should reappear in the user's journal by configured recurrence
|
/// The time after which this request should reappear in the user's journal by configured recurrence
|
||||||
showAfter : Instant
|
showAfter : Instant
|
||||||
/// The type of recurrence for this request
|
/// The recurrence for this request
|
||||||
recurType : Recurrence
|
recurrence : Recurrence
|
||||||
/// How many of the recurrence intervals should occur between appearances in the journal
|
|
||||||
recurCount : int16
|
|
||||||
/// History entries for the request
|
/// History entries for the request
|
||||||
history : History list
|
history : History list
|
||||||
/// Note entries for the request
|
/// Note entries for the request
|
||||||
@@ -180,8 +175,7 @@ module JournalRequest =
|
|||||||
lastStatus = match hist with Some h -> h.status | None -> Created
|
lastStatus = match hist with Some h -> h.status | None -> Created
|
||||||
snoozedUntil = req.snoozedUntil
|
snoozedUntil = req.snoozedUntil
|
||||||
showAfter = req.showAfter
|
showAfter = req.showAfter
|
||||||
recurType = req.recurType
|
recurrence = req.recurrence
|
||||||
recurCount = req.recurCount
|
|
||||||
history = []
|
history = []
|
||||||
notes = []
|
notes = []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ module Error =
|
|||||||
log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.")
|
log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.")
|
||||||
clearResponse
|
clearResponse
|
||||||
>=> setStatusCode 500
|
>=> setStatusCode 500
|
||||||
>=> setHttpHeader "X-Toast" (sprintf "error|||%s: %s" (ex.GetType().Name) ex.Message)
|
>=> setHttpHeader "X-Toast" $"error|||{ex.GetType().Name}: {ex.Message}"
|
||||||
>=> text ex.Message
|
>=> text ex.Message
|
||||||
|
|
||||||
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized reponse
|
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response
|
||||||
let notAuthorized : HttpHandler =
|
let notAuthorized : HttpHandler =
|
||||||
fun next ctx ->
|
fun next ctx ->
|
||||||
(next, ctx)
|
(next, ctx)
|
||||||
@@ -97,7 +97,7 @@ module private Helpers =
|
|||||||
/// Return a 201 CREATED response with the location header set for the created resource
|
/// Return a 201 CREATED response with the location header set for the created resource
|
||||||
let createdAt url : HttpHandler =
|
let createdAt url : HttpHandler =
|
||||||
fun next ctx ->
|
fun next ctx ->
|
||||||
(sprintf "%s://%s%s" ctx.Request.Scheme ctx.Request.Host.Value url |> setHttpHeader HeaderNames.Location
|
($"{ctx.Request.Scheme}://{ctx.Request.Host.Value}{url}" |> setHttpHeader HeaderNames.Location
|
||||||
>=> created) next ctx
|
>=> created) next ctx
|
||||||
|
|
||||||
/// Return a 303 SEE OTHER response (forces a GET on the redirected URL)
|
/// Return a 303 SEE OTHER response (forces a GET on the redirected URL)
|
||||||
@@ -107,7 +107,7 @@ module private Helpers =
|
|||||||
/// Render a component result
|
/// Render a component result
|
||||||
let renderComponent nodes : HttpHandler =
|
let renderComponent nodes : HttpHandler =
|
||||||
noResponseCaching
|
noResponseCaching
|
||||||
>=> fun next ctx -> backgroundTask {
|
>=> fun _ ctx -> backgroundTask {
|
||||||
return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
|
return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ module private Helpers =
|
|||||||
|
|
||||||
/// Composable handler to write a view to the output
|
/// Composable handler to write a view to the output
|
||||||
let writeView view : HttpHandler =
|
let writeView view : HttpHandler =
|
||||||
fun next ctx -> backgroundTask {
|
fun _ ctx -> backgroundTask {
|
||||||
return! ctx.WriteHtmlViewAsync view
|
return! ctx.WriteHtmlViewAsync view
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ module private Helpers =
|
|||||||
module Messages =
|
module Messages =
|
||||||
|
|
||||||
/// The messages being held
|
/// The messages being held
|
||||||
let mutable private messages : Map<string, (string * string)> = Map.empty
|
let mutable private messages : Map<string, string * string> = Map.empty
|
||||||
|
|
||||||
/// Locked update to prevent updates by multiple threads
|
/// Locked update to prevent updates by multiple threads
|
||||||
let private upd8 = obj ()
|
let private upd8 = obj ()
|
||||||
@@ -150,7 +150,7 @@ module private Helpers =
|
|||||||
|
|
||||||
/// Add a success message header to the response
|
/// Add a success message header to the response
|
||||||
let pushSuccess ctx message url =
|
let pushSuccess ctx message url =
|
||||||
push ctx (sprintf "success|||%s" message) url
|
push ctx $"success|||{message}" url
|
||||||
|
|
||||||
/// Pop the messages for the given user
|
/// Pop the messages for the given user
|
||||||
let pop userId = lock upd8 (fun () ->
|
let pop userId = lock upd8 (fun () ->
|
||||||
@@ -289,7 +289,7 @@ module Journal =
|
|||||||
|> Option.map (fun c -> c.Value)
|
|> Option.map (fun c -> c.Value)
|
||||||
|> Option.defaultValue "Your"
|
|> Option.defaultValue "Your"
|
||||||
let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's"
|
let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's"
|
||||||
return! partial (sprintf "%s Prayer Journal" title) (Views.Journal.journal usr) next ctx
|
return! partial $"{title} Prayer Journal" (Views.Journal.journal usr) next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -343,9 +343,9 @@ module Request =
|
|||||||
let now = now ctx
|
let now = now ctx
|
||||||
do! Data.addHistory reqId usrId { asOf = now; status = Prayed; text = None } db
|
do! Data.addHistory reqId usrId { asOf = now; status = Prayed; text = None } db
|
||||||
let nextShow =
|
let nextShow =
|
||||||
match Recurrence.duration req.recurType with
|
match Recurrence.duration req.recurrence with
|
||||||
| 0L -> Instant.MinValue
|
| 0L -> Instant.MinValue
|
||||||
| duration -> now.Plus (Duration.FromSeconds (duration * int64 req.recurCount))
|
| duration -> now.Plus (Duration.FromSeconds duration)
|
||||||
do! Data.updateShowAfter reqId usrId nextShow db
|
do! Data.updateShowAfter reqId usrId nextShow db
|
||||||
do! db.saveChanges ()
|
do! db.saveChanges ()
|
||||||
return! (withSuccessMessage "Request marked as prayed" >=> Components.journalItems) next ctx
|
return! (withSuccessMessage "Request marked as prayed" >=> Components.journalItems) next ctx
|
||||||
@@ -465,27 +465,25 @@ module Request =
|
|||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive a recurrence and interval from its primitive representation in the form
|
/// Derive a recurrence from its representation in the form
|
||||||
let private parseRecurrence (form : Models.Request) =
|
let private parseRecurrence (form : Models.Request) =
|
||||||
(Recurrence.ofString (match form.recurInterval with Some x -> x | _ -> "Immediate"),
|
match form.recurInterval with Some x -> $"{defaultArg form.recurCount 0s} {x}" | None -> "Immediate"
|
||||||
defaultArg form.recurCount (int16 0))
|
|> Recurrence.ofString
|
||||||
|
|
||||||
// POST /request
|
// POST /request
|
||||||
let add : HttpHandler =
|
let add : HttpHandler =
|
||||||
requiresAuthentication Error.notAuthorized
|
requiresAuthentication Error.notAuthorized
|
||||||
>=> fun next ctx -> backgroundTask {
|
>=> fun next ctx -> backgroundTask {
|
||||||
let! form = ctx.BindModelAsync<Models.Request> ()
|
let! form = ctx.BindModelAsync<Models.Request> ()
|
||||||
let db = db ctx
|
let db = db ctx
|
||||||
let usrId = userId ctx
|
let usrId = userId ctx
|
||||||
let now = now ctx
|
let now = now ctx
|
||||||
let (recur, interval) = parseRecurrence form
|
|
||||||
let req =
|
let req =
|
||||||
{ Request.empty with
|
{ Request.empty with
|
||||||
userId = usrId
|
userId = usrId
|
||||||
enteredOn = now
|
enteredOn = now
|
||||||
showAfter = Instant.MinValue
|
showAfter = Instant.MinValue
|
||||||
recurType = recur
|
recurrence = parseRecurrence form
|
||||||
recurCount = interval
|
|
||||||
history = [
|
history = [
|
||||||
{ asOf = now
|
{ asOf = now
|
||||||
status = Created
|
status = Created
|
||||||
@@ -509,11 +507,11 @@ module Request =
|
|||||||
match! Data.tryJournalById (RequestId.ofString form.requestId) usrId db with
|
match! Data.tryJournalById (RequestId.ofString form.requestId) usrId db with
|
||||||
| Some req ->
|
| Some req ->
|
||||||
// update recurrence if changed
|
// update recurrence if changed
|
||||||
let (recur, interval) = parseRecurrence form
|
let recur = parseRecurrence form
|
||||||
match recur = req.recurType && interval = req.recurCount with
|
match recur = req.recurrence with
|
||||||
| true -> ()
|
| true -> ()
|
||||||
| false ->
|
| false ->
|
||||||
do! Data.updateRecurrence req.requestId usrId recur interval db
|
do! Data.updateRecurrence req.requestId usrId recur db
|
||||||
match recur with
|
match recur with
|
||||||
| Immediate -> do! Data.updateShowAfter req.requestId usrId Instant.MinValue db
|
| Immediate -> do! Data.updateShowAfter req.requestId usrId Instant.MinValue db
|
||||||
| _ -> ()
|
| _ -> ()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Version>3.0.0.0</Version>
|
<Version>3.0.0.0</Version>
|
||||||
|
<NoWarn>3391</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Domain.fs" />
|
<Compile Include="Domain.fs" />
|
||||||
@@ -19,8 +20,8 @@
|
|||||||
<PackageReference Include="FSharp.SystemTextJson" Version="0.17.4" />
|
<PackageReference Include="FSharp.SystemTextJson" Version="0.17.4" />
|
||||||
<PackageReference Include="FunctionalCuid" Version="1.0.0" />
|
<PackageReference Include="FunctionalCuid" Version="1.0.0" />
|
||||||
<PackageReference Include="Giraffe" Version="5.0.0" />
|
<PackageReference Include="Giraffe" Version="5.0.0" />
|
||||||
<PackageReference Include="Giraffe.Htmx" Version="0.9.2" />
|
<PackageReference Include="Giraffe.Htmx" Version="1.6.1" />
|
||||||
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="0.9.2" />
|
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.6.1" />
|
||||||
<PackageReference Include="LiteDB" Version="5.0.11" />
|
<PackageReference Include="LiteDB" Version="5.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.10" />
|
||||||
<PackageReference Include="NodaTime" Version="3.0.9" />
|
<PackageReference Include="NodaTime" Version="3.0.9" />
|
||||||
|
|||||||
@@ -84,17 +84,17 @@ module Configure =
|
|||||||
opts.OnAppendCookie <- fun ctx -> sameSite ctx.CookieOptions
|
opts.OnAppendCookie <- fun ctx -> sameSite ctx.CookieOptions
|
||||||
opts.OnDeleteCookie <- fun ctx -> sameSite ctx.CookieOptions)
|
opts.OnDeleteCookie <- fun ctx -> sameSite ctx.CookieOptions)
|
||||||
.AddAuthentication(
|
.AddAuthentication(
|
||||||
/// Use HTTP "Bearer" authentication with JWTs
|
// Use HTTP "Bearer" authentication with JWTs
|
||||||
fun opts ->
|
fun opts ->
|
||||||
opts.DefaultAuthenticateScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
opts.DefaultAuthenticateScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
||||||
opts.DefaultSignInScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
opts.DefaultSignInScheme <- CookieAuthenticationDefaults.AuthenticationScheme
|
||||||
opts.DefaultChallengeScheme <- CookieAuthenticationDefaults.AuthenticationScheme)
|
opts.DefaultChallengeScheme <- CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
.AddCookie()
|
.AddCookie()
|
||||||
.AddOpenIdConnect("Auth0",
|
.AddOpenIdConnect("Auth0",
|
||||||
/// Configure OIDC with Auth0 options from configuration
|
// Configure OIDC with Auth0 options from configuration
|
||||||
fun opts ->
|
fun opts ->
|
||||||
let cfg = bldr.Configuration.GetSection "Auth0"
|
let cfg = bldr.Configuration.GetSection "Auth0"
|
||||||
opts.Authority <- sprintf "https://%s/" cfg["Domain"]
|
opts.Authority <- $"""https://{cfg["Domain"]}/"""
|
||||||
opts.ClientId <- cfg["Id"]
|
opts.ClientId <- cfg["Id"]
|
||||||
opts.ClientSecret <- cfg["Secret"]
|
opts.ClientSecret <- cfg["Secret"]
|
||||||
opts.ResponseType <- OpenIdConnectResponseType.Code
|
opts.ResponseType <- OpenIdConnectResponseType.Code
|
||||||
@@ -118,11 +118,10 @@ module Configure =
|
|||||||
| true ->
|
| true ->
|
||||||
// transform to absolute
|
// transform to absolute
|
||||||
let request = ctx.Request
|
let request = ctx.Request
|
||||||
sprintf "%s://%s%s%s" request.Scheme request.Host.Value request.PathBase.Value redirUri
|
$"{request.Scheme}://{request.Host.Value}{request.PathBase.Value}{redirUri}"
|
||||||
| false -> redirUri
|
| false -> redirUri
|
||||||
Uri.EscapeDataString finalRedirUri |> sprintf "&returnTo=%s"
|
Uri.EscapeDataString $"&returnTo={finalRedirUri}"
|
||||||
sprintf "https://%s/v2/logout?client_id=%s%s" cfg["Domain"] cfg["Id"] returnTo
|
ctx.Response.Redirect $"""https://{cfg["Domain"]}/v2/logout?client_id={cfg["Id"]}{returnTo}"""
|
||||||
|> ctx.Response.Redirect
|
|
||||||
ctx.HandleResponse ()
|
ctx.HandleResponse ()
|
||||||
|
|
||||||
Task.CompletedTask
|
Task.CompletedTask
|
||||||
@@ -159,7 +158,7 @@ module Configure =
|
|||||||
.UseRouting()
|
.UseRouting()
|
||||||
.UseAuthentication()
|
.UseAuthentication()
|
||||||
.UseGiraffeErrorHandler(Handlers.Error.error)
|
.UseGiraffeErrorHandler(Handlers.Error.error)
|
||||||
.UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes |> ignore)
|
.UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes)
|
||||||
|> ignore
|
|> ignore
|
||||||
app
|
app
|
||||||
|
|
||||||
|
|||||||
@@ -110,11 +110,7 @@ let htmlFoot =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
script [
|
Htmx.Script.minified
|
||||||
_src "https://unpkg.com/htmx.org@1.5.0"
|
|
||||||
_integrity "sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI"
|
|
||||||
_crossorigin "anonymous"
|
|
||||||
] []
|
|
||||||
script [] [
|
script [] [
|
||||||
rawText "if (!htmx) document.write('<script src=\"/script/htmx-1.5.0.min.js\"><\/script>')"
|
rawText "if (!htmx) document.write('<script src=\"/script/htmx-1.5.0.min.js\"><\/script>')"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ open Giraffe.ViewEngine
|
|||||||
open Giraffe.ViewEngine.Htmx
|
open Giraffe.ViewEngine.Htmx
|
||||||
open MyPrayerJournal
|
open MyPrayerJournal
|
||||||
open NodaTime
|
open NodaTime
|
||||||
open System
|
|
||||||
|
|
||||||
/// Create a request within the list
|
/// Create a request within the list
|
||||||
let reqListItem now req =
|
let reqListItem now req =
|
||||||
@@ -142,6 +141,14 @@ let edit (req : JournalRequest) returnTo isNew =
|
|||||||
| "active" -> "/requests/active"
|
| "active" -> "/requests/active"
|
||||||
| "snoozed" -> "/requests/snoozed"
|
| "snoozed" -> "/requests/snoozed"
|
||||||
| _ (* "journal" *) -> "/journal"
|
| _ (* "journal" *) -> "/journal"
|
||||||
|
let recurCount =
|
||||||
|
match req.recurrence with
|
||||||
|
| Immediate -> None
|
||||||
|
| Hours h -> Some h
|
||||||
|
| Days d -> Some d
|
||||||
|
| Weeks w -> Some w
|
||||||
|
|> Option.map string
|
||||||
|
|> Option.defaultValue ""
|
||||||
article [ _class "container" ] [
|
article [ _class "container" ] [
|
||||||
h2 [ _class "pb-3" ] [ (match isNew with true -> "Add" | false -> "Edit") |> strf "%s Prayer Request" ]
|
h2 [ _class "pb-3" ] [ (match isNew with true -> "Add" | false -> "Edit") |> strf "%s Prayer Request" ]
|
||||||
form [
|
form [
|
||||||
@@ -202,7 +209,7 @@ let edit (req : JournalRequest) returnTo isNew =
|
|||||||
_name "recurType"
|
_name "recurType"
|
||||||
_value "Immediate"
|
_value "Immediate"
|
||||||
_onclick "mpj.edit.toggleRecurrence(event)"
|
_onclick "mpj.edit.toggleRecurrence(event)"
|
||||||
match req.recurType with Immediate -> _checked | _ -> ()
|
match req.recurrence with Immediate -> _checked | _ -> ()
|
||||||
]
|
]
|
||||||
label [ _for "rI" ] [ str "Immediately" ]
|
label [ _for "rI" ] [ str "Immediately" ]
|
||||||
]
|
]
|
||||||
@@ -214,7 +221,7 @@ let edit (req : JournalRequest) returnTo isNew =
|
|||||||
_name "recurType"
|
_name "recurType"
|
||||||
_value "Other"
|
_value "Other"
|
||||||
_onclick "mpj.edit.toggleRecurrence(event)"
|
_onclick "mpj.edit.toggleRecurrence(event)"
|
||||||
match req.recurType with Immediate -> () | _ -> _checked
|
match req.recurrence with Immediate -> () | _ -> _checked
|
||||||
]
|
]
|
||||||
label [ _for "rO" ] [ rawText "Every…" ]
|
label [ _for "rO" ] [ rawText "Every…" ]
|
||||||
]
|
]
|
||||||
@@ -225,10 +232,10 @@ let edit (req : JournalRequest) returnTo isNew =
|
|||||||
_id "recurCount"
|
_id "recurCount"
|
||||||
_name "recurCount"
|
_name "recurCount"
|
||||||
_placeholder "0"
|
_placeholder "0"
|
||||||
_value (string req.recurCount)
|
_value recurCount
|
||||||
_style "width:6rem;"
|
_style "width:6rem;"
|
||||||
_required
|
_required
|
||||||
match req.recurType with Immediate -> _disabled | _ -> ()
|
match req.recurrence with Immediate -> _disabled | _ -> ()
|
||||||
]
|
]
|
||||||
label [ _for "recurCount" ] [ str "Count" ]
|
label [ _for "recurCount" ] [ str "Count" ]
|
||||||
]
|
]
|
||||||
@@ -239,11 +246,11 @@ let edit (req : JournalRequest) returnTo isNew =
|
|||||||
_name "recurInterval"
|
_name "recurInterval"
|
||||||
_style "width:6rem;"
|
_style "width:6rem;"
|
||||||
_required
|
_required
|
||||||
match req.recurType with Immediate -> _disabled | _ -> ()
|
match req.recurrence with Immediate -> _disabled | _ -> ()
|
||||||
] [
|
] [
|
||||||
option [ _value "Hours"; match req.recurType with Hours -> _selected | _ -> () ] [ str "hours" ]
|
option [ _value "Hours"; match req.recurrence with Hours _ -> _selected | _ -> () ] [ str "hours" ]
|
||||||
option [ _value "Days"; match req.recurType with Days -> _selected | _ -> () ] [ str "days" ]
|
option [ _value "Days"; match req.recurrence with Days _ -> _selected | _ -> () ] [ str "days" ]
|
||||||
option [ _value "Weeks"; match req.recurType with Weeks -> _selected | _ -> () ] [ str "weeks" ]
|
option [ _value "Weeks"; match req.recurrence with Weeks _ -> _selected | _ -> () ] [ str "weeks" ]
|
||||||
]
|
]
|
||||||
label [ _form "recurInterval" ] [ str "Interval" ]
|
label [ _form "recurInterval" ] [ str "Interval" ]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else{e.htmx=t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var D={onLoad:t,process:nt,on:N,off:I,trigger:ft,ajax:Jt,find:w,findAll:S,closest:L,values:function(e,t){var r=Tt(e,t||"post");return r.values},remove:E,addClass:C,removeClass:q,toggleClass:R,takeClass:O,defineExtension:er,removeExtension:tr,logAll:b,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth"},parseInterval:h,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){return new WebSocket(e,[])},version:"1.5.0"};var r=["get","post","put","delete","patch"];var n=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function h(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}return parseFloat(e)||undefined}function c(e,t){return e.getAttribute&&e.getAttribute(t)}function s(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function F(e,t){return c(e,t)||c(e,"data-"+t)}function l(e){return e.parentElement}function X(){return document}function d(e,t){if(t(e)){return e}else if(l(e)){return d(l(e),t)}else{return null}}function P(e,t){var r=null;d(e,function(e){return r=F(e,t)});return r}function v(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function i(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function o(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=X().createDocumentFragment()}return i}function u(e){if(D.config.useTemplateFragments){var t=o("<body><template>"+e+"</template></body>",0);return t.querySelector("template").content}else{var r=i(e);switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return o("<table>"+e+"</table>",1);case"col":return o("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return o("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return o("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":return o("<div>"+e+"</div>",1);default:return o(e,0)}}}function U(e){if(e){e()}}function a(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function f(e){return a(e,"Function")}function g(e){return a(e,"Object")}function j(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function p(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function B(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function m(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function z(e){return X().body.contains(e)}function y(e){return e.trim().split(/\s+/)}function V(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function x(e){try{return JSON.parse(e)}catch(e){lt(e);return null}}function e(e){return jt(X().body,function(){return eval(e)})}function t(t){var e=D.on("htmx:load",function(e){t(e.detail.elt)});return e}function b(){D.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function w(e,t){if(t){return e.querySelector(t)}else{return w(X(),e)}}function S(e,t){if(t){return e.querySelectorAll(t)}else{return S(X(),e)}}function E(e,t){e=H(e);if(t){setTimeout(function(){E(e)},t)}else{e.parentElement.removeChild(e)}}function C(e,t,r){e=H(e);if(r){setTimeout(function(){C(e,t)},r)}else{e.classList.add(t)}}function q(e,t,r){e=H(e);if(r){setTimeout(function(){q(e,t)},r)}else{e.classList.remove(t)}}function R(e,t){e=H(e);e.classList.toggle(t)}function O(e,t){e=H(e);B(e.parentElement.children,function(e){q(e,t)});C(e,t)}function L(e,t){e=H(e);if(e.closest){return e.closest(t)}else{do{if(e==null||v(e,t)){return e}}while(e=e&&l(e))}}function T(e,t){if(t.indexOf("closest ")===0){return[L(e,t.substr(8))]}else if(t.indexOf("find ")===0){return[w(e,t.substr(5))]}else if(t==="document"){return[document]}else{return X().querySelectorAll(t)}}function A(e,t){return T(e,t)[0]}function H(e){if(a(e,"String")){return w(e)}else{return e}}function k(e,t,r){if(f(t)){return{target:X().body,event:e,listener:t}}else{return{target:H(e),event:t,listener:r}}}function N(t,r,n){nr(function(){var e=k(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=f(r);return e?r:n}function I(t,r,n){Line truncated
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else{e.htmx=t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var D={onLoad:t,process:rt,on:N,off:I,trigger:lt,ajax:$t,find:w,findAll:S,closest:O,values:function(e,t){var r=Ot(e,t||"post");return r.values},remove:E,addClass:C,removeClass:R,toggleClass:q,takeClass:L,defineExtension:Qt,removeExtension:er,logAll:b,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth"},parseInterval:h,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){return new WebSocket(e,[])},version:"1.6.1"};var r=["get","post","put","delete","patch"];var n=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function h(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}return parseFloat(e)||undefined}function c(e,t){return e.getAttribute&&e.getAttribute(t)}function s(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function F(e,t){return c(e,t)||c(e,"data-"+t)}function l(e){return e.parentElement}function P(){return document}function d(e,t){if(t(e)){return e}else if(l(e)){return d(l(e),t)}else{return null}}function X(e,t){var r=null;d(e,function(e){return r=F(e,t)});if(r!=="unset"){return r}}function v(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function i(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function o(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=P().createDocumentFragment()}return i}function u(e){if(D.config.useTemplateFragments){var t=o("<body><template>"+e+"</template></body>",0);return t.querySelector("template").content}else{var r=i(e);switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return o("<table>"+e+"</table>",1);case"col":return o("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return o("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return o("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":return o("<div>"+e+"</div>",1);default:return o(e,0)}}}function U(e){if(e){e()}}function a(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function f(e){return a(e,"Function")}function g(e){return a(e,"Object")}function j(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function p(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function B(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function m(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function z(e){return P().body.contains(e)}function y(e){return e.trim().split(/\s+/)}function V(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function x(e){try{return JSON.parse(e)}catch(e){ut(e);return null}}function e(e){return Ut(P().body,function(){return eval(e)})}function t(t){var e=D.on("htmx:load",function(e){t(e.detail.elt)});return e}function b(){D.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function w(e,t){if(t){return e.querySelector(t)}else{return w(P(),e)}}function S(e,t){if(t){return e.querySelectorAll(t)}else{return S(P(),e)}}function E(e,t){e=H(e);if(t){setTimeout(function(){E(e)},t)}else{e.parentElement.removeChild(e)}}function C(e,t,r){e=H(e);if(r){setTimeout(function(){C(e,t)},r)}else{e.classList&&e.classList.add(t)}}function R(e,t,r){e=H(e);if(r){setTimeout(function(){R(e,t)},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function q(e,t){e=H(e);e.classList.toggle(t)}function L(e,t){e=H(e);B(e.parentElement.children,function(e){R(e,t)});C(e,t)}function O(e,t){e=H(e);if(e.closest){return e.closest(t)}else{do{if(e==null||v(e,t)){return e}}while(e=e&&l(e))}}function T(e,t){if(t.indexOf("closest ")===0){return[O(e,t.substr(8))]}else if(t.indexOf("find ")===0){return[w(e,t.substr(5))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return P().querySelectorAll(t)}}function A(e,t){if(t){return T(e,t)[0]}else{return T(P().body,e)[0]}}function H(e){if(a(e,"String")){return w(e)}else{return e}}function k(e,t,r){if(f(t)){return{target:P().body,eveLine truncated
|
||||||
Reference in new issue
Block a user