WIP on update to .NET 8 (#75)
This commit is contained in:
		
							parent
							
								
									20dcaf6e1b
								
							
						
					
					
						commit
						b3f62c2586
					
				@ -5,8 +5,6 @@ VisualStudioVersion = 16.0.30114.105
 | 
			
		||||
MinimumVisualStudioVersion = 10.0.40219.1
 | 
			
		||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyPrayerJournal", "MyPrayerJournal\MyPrayerJournal.fsproj", "{6BD5A3C8-F859-42A0-ACD7-A5819385E828}"
 | 
			
		||||
EndProject
 | 
			
		||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyPrayerJournal.ToPostgres", "MyPrayerJournal.ToPostgres\MyPrayerJournal.ToPostgres.fsproj", "{3114B8F4-E388-4804-94D3-A2F4D42797C6}"
 | 
			
		||||
EndProject
 | 
			
		||||
Global
 | 
			
		||||
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 | 
			
		||||
		Debug|Any CPU = Debug|Any CPU
 | 
			
		||||
@ -24,9 +22,5 @@ Global
 | 
			
		||||
		{72B57736-8721-4636-A309-49FA4222416E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 | 
			
		||||
		{72B57736-8721-4636-A309-49FA4222416E}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{72B57736-8721-4636-A309-49FA4222416E}.Release|Any CPU.Build.0 = Release|Any CPU
 | 
			
		||||
		{3114B8F4-E388-4804-94D3-A2F4D42797C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 | 
			
		||||
		{3114B8F4-E388-4804-94D3-A2F4D42797C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
 | 
			
		||||
		{3114B8F4-E388-4804-94D3-A2F4D42797C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
			
		||||
		{3114B8F4-E388-4804-94D3-A2F4D42797C6}.Release|Any CPU.Build.0 = Release|Any CPU
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
EndGlobal
 | 
			
		||||
 | 
			
		||||
@ -15,24 +15,24 @@ module Json =
 | 
			
		||||
    open System.Text.Json.Serialization
 | 
			
		||||
 | 
			
		||||
    /// Convert a wrapped DU to/from its string representation
 | 
			
		||||
    type WrappedJsonConverter<'T> (wrap : string -> 'T, unwrap : 'T -> string) =
 | 
			
		||||
        inherit JsonConverter<'T> ()
 | 
			
		||||
    type WrappedJsonConverter<'T>(wrap : string -> 'T, unwrap : 'T -> string) =
 | 
			
		||||
        inherit JsonConverter<'T>()
 | 
			
		||||
        override _.Read(reader, _, _) =
 | 
			
		||||
            wrap (reader.GetString ()) 
 | 
			
		||||
            wrap (reader.GetString()) 
 | 
			
		||||
        override _.Write(writer, value, _) =
 | 
			
		||||
            writer.WriteStringValue (unwrap value)
 | 
			
		||||
            writer.WriteStringValue(unwrap value)
 | 
			
		||||
    
 | 
			
		||||
    open System.Text.Json
 | 
			
		||||
    open NodaTime.Serialization.SystemTextJson
 | 
			
		||||
 | 
			
		||||
    /// JSON serializer options to support the target domain
 | 
			
		||||
    let options =
 | 
			
		||||
        let opts = JsonSerializerOptions ()
 | 
			
		||||
        [   WrappedJsonConverter (Recurrence.ofString,    Recurrence.toString) :> JsonConverter
 | 
			
		||||
            WrappedJsonConverter (RequestAction.ofString, RequestAction.toString)
 | 
			
		||||
            WrappedJsonConverter (RequestId.ofString,     RequestId.toString)
 | 
			
		||||
            WrappedJsonConverter (UserId,                 UserId.toString)
 | 
			
		||||
            JsonFSharpConverter  ()
 | 
			
		||||
        let opts = JsonSerializerOptions()
 | 
			
		||||
        [   WrappedJsonConverter(Recurrence.ofString,    Recurrence.toString) :> JsonConverter
 | 
			
		||||
            WrappedJsonConverter(RequestAction.ofString, RequestAction.toString)
 | 
			
		||||
            WrappedJsonConverter(RequestId.ofString,     RequestId.toString)
 | 
			
		||||
            WrappedJsonConverter(UserId,                 UserId.toString)
 | 
			
		||||
            JsonFSharpConverter()
 | 
			
		||||
        ]
 | 
			
		||||
        |> List.iter opts.Converters.Add
 | 
			
		||||
        let _ = opts.ConfigureForNodaTime NodaTime.DateTimeZoneProviders.Tzdb
 | 
			
		||||
@ -62,12 +62,12 @@ module Connection =
 | 
			
		||||
    /// Set up the data environment
 | 
			
		||||
    let setUp (cfg : IConfiguration) = backgroundTask {
 | 
			
		||||
        let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "mpj")
 | 
			
		||||
        let _ = builder.UseNodaTime ()
 | 
			
		||||
        Configuration.useDataSource (builder.Build ())
 | 
			
		||||
        let _ = builder.UseNodaTime()
 | 
			
		||||
        Configuration.useDataSource (builder.Build())
 | 
			
		||||
        Configuration.useSerializer
 | 
			
		||||
            { new IDocumentSerializer with
 | 
			
		||||
                member _.Serialize<'T>   (it : 'T)     = JsonSerializer.Serialize       (it, Json.options)
 | 
			
		||||
                member _.Deserialize<'T> (it : string) = JsonSerializer.Deserialize<'T> (it, Json.options)
 | 
			
		||||
                member _.Serialize<'T>(it : 'T)       = JsonSerializer.Serialize(it, Json.options)
 | 
			
		||||
                member _.Deserialize<'T>(it : string) = JsonSerializer.Deserialize<'T>(it, Json.options)
 | 
			
		||||
            }
 | 
			
		||||
        do! ensureDb ()
 | 
			
		||||
    }
 | 
			
		||||
@ -80,9 +80,8 @@ module Request =
 | 
			
		||||
    open NodaTime
 | 
			
		||||
 | 
			
		||||
    /// Add a request
 | 
			
		||||
    let add req = backgroundTask {
 | 
			
		||||
        do! insert Table.Request (RequestId.toString req.Id) req
 | 
			
		||||
    }
 | 
			
		||||
    let add req =
 | 
			
		||||
        insert<Request> Table.Request req
 | 
			
		||||
 | 
			
		||||
    /// Does a request exist for the given request ID and user ID?
 | 
			
		||||
    let existsById (reqId : RequestId) (userId : UserId) =
 | 
			
		||||
@ -100,7 +99,7 @@ module Request =
 | 
			
		||||
        let dbId = RequestId.toString reqId
 | 
			
		||||
        match! existsById reqId userId with
 | 
			
		||||
        | true -> do! Update.partialById Table.Request dbId {| Recurrence = recurType |}
 | 
			
		||||
        | false -> invalidOp "Request ID {dbId} not found"
 | 
			
		||||
        | false -> invalidOp $"Request ID {dbId} not found"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Update the show-after time for a request
 | 
			
		||||
@ -108,7 +107,7 @@ module Request =
 | 
			
		||||
        let dbId = RequestId.toString reqId
 | 
			
		||||
        match! existsById reqId userId with
 | 
			
		||||
        | true -> do! Update.partialById Table.Request dbId {| ShowAfter = showAfter |}
 | 
			
		||||
        | false -> invalidOp "Request ID {dbId} not found"
 | 
			
		||||
        | false -> invalidOp $"Request ID {dbId} not found"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Update the snoozed and show-after values for a request
 | 
			
		||||
@ -116,7 +115,7 @@ module Request =
 | 
			
		||||
        let dbId = RequestId.toString reqId
 | 
			
		||||
        match! existsById reqId userId with
 | 
			
		||||
        | true -> do! Update.partialById Table.Request dbId {| SnoozedUntil = until; ShowAfter = until |}
 | 
			
		||||
        | false -> invalidOp "Request ID {dbId} not found"
 | 
			
		||||
        | false -> invalidOp $"Request ID {dbId} not found"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -130,7 +129,7 @@ module History =
 | 
			
		||||
        match! Request.tryById reqId userId with
 | 
			
		||||
        | Some req ->
 | 
			
		||||
            do! Update.partialById Table.Request dbId
 | 
			
		||||
                                   {| History = (hist :: req.History) |> List.sortByDescending (fun it -> it.AsOf) |}
 | 
			
		||||
                                   {| History = (hist :: req.History) |> List.sortByDescending (_.AsOf) |}
 | 
			
		||||
        | None -> invalidOp $"Request ID {dbId} not found"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -152,7 +151,7 @@ module Journal =
 | 
			
		||||
            |> Seq.ofList
 | 
			
		||||
            |> Seq.map JournalRequest.ofRequestLite
 | 
			
		||||
            |> Seq.filter (fun it -> it.LastStatus = Answered)
 | 
			
		||||
            |> Seq.sortByDescending (fun it -> it.AsOf)
 | 
			
		||||
            |> Seq.sortByDescending (_.AsOf)
 | 
			
		||||
            |> List.ofSeq
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -169,7 +168,7 @@ module Journal =
 | 
			
		||||
            |> Seq.ofList
 | 
			
		||||
            |> Seq.map JournalRequest.ofRequestLite
 | 
			
		||||
            |> Seq.filter (fun it -> it.LastStatus <> Answered)
 | 
			
		||||
            |> Seq.sortBy (fun it -> it.AsOf)
 | 
			
		||||
            |> Seq.sortBy (_.AsOf)
 | 
			
		||||
            |> List.ofSeq
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -195,7 +194,7 @@ module Note =
 | 
			
		||||
        match! Request.tryById reqId userId with
 | 
			
		||||
        | Some req ->
 | 
			
		||||
            do! Update.partialById Table.Request dbId
 | 
			
		||||
                                   {| Notes = (note :: req.Notes) |> List.sortByDescending (fun it -> it.AsOf) |}
 | 
			
		||||
                                   {| Notes = (note :: req.Notes) |> List.sortByDescending (_.AsOf) |}
 | 
			
		||||
        | None -> invalidOp $"Request ID {dbId} not found"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -244,14 +244,14 @@ module JournalRequest =
 | 
			
		||||
        //    them at the bottom of the list.
 | 
			
		||||
        //  - Snoozed requests will reappear at the bottom of the list when they return.
 | 
			
		||||
        //  - New requests will go to the bottom of the list, but will rise as others are marked as prayed.
 | 
			
		||||
        let lastActivity = lastHistory |> Option.map (fun it -> it.AsOf) |> Option.defaultValue Instant.MinValue
 | 
			
		||||
        let lastActivity = lastHistory |> Option.map (_.AsOf) |> Option.defaultValue Instant.MinValue
 | 
			
		||||
        let showAfter    = defaultArg req.ShowAfter    Instant.MinValue
 | 
			
		||||
        let snoozedUntil = defaultArg req.SnoozedUntil Instant.MinValue
 | 
			
		||||
        let lastPrayed   =
 | 
			
		||||
            history
 | 
			
		||||
            |> Seq.filter History.isPrayed
 | 
			
		||||
            |> Seq.tryHead
 | 
			
		||||
            |> Option.map (fun it -> it.AsOf)
 | 
			
		||||
            |> Option.map (_.AsOf)
 | 
			
		||||
            |> Option.defaultValue Instant.MinValue
 | 
			
		||||
        let asOf = List.max [ lastPrayed; showAfter; snoozedUntil ]
 | 
			
		||||
        {   RequestId    = req.Id
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ module private LogOnHelpers =
 | 
			
		||||
    let logOn url : HttpHandler = fun next ctx -> task {
 | 
			
		||||
        match url with
 | 
			
		||||
        | Some it ->
 | 
			
		||||
            do! ctx.ChallengeAsync ("Auth0", AuthenticationProperties (RedirectUri = it))
 | 
			
		||||
            do! ctx.ChallengeAsync("Auth0", AuthenticationProperties(RedirectUri = it))
 | 
			
		||||
            return! next ctx
 | 
			
		||||
        | None -> return! challenge "Auth0" next ctx
 | 
			
		||||
    }
 | 
			
		||||
@ -57,14 +57,14 @@ type HttpContext with
 | 
			
		||||
        |> Option.ofObj
 | 
			
		||||
        |> Option.map (fun user -> user.Claims |> Seq.tryFind (fun u -> u.Type = ClaimTypes.NameIdentifier))
 | 
			
		||||
        |> Option.flatten
 | 
			
		||||
        |> Option.map (fun claim -> claim.Value)
 | 
			
		||||
        |> Option.map (_.Value)
 | 
			
		||||
    
 | 
			
		||||
    /// The current user's ID
 | 
			
		||||
    //  NOTE: this may raise if you don't run the request through the requireUser handler first
 | 
			
		||||
    member this.UserId = UserId this.CurrentUser.Value
 | 
			
		||||
    
 | 
			
		||||
    /// The system clock
 | 
			
		||||
    member this.Clock = this.GetService<IClock> ()
 | 
			
		||||
    member this.Clock = this.GetService<IClock>()
 | 
			
		||||
    
 | 
			
		||||
    /// Get the current instant from the system clock
 | 
			
		||||
    member this.Now = this.Clock.GetCurrentInstant
 | 
			
		||||
@ -94,7 +94,7 @@ module private Helpers =
 | 
			
		||||
    
 | 
			
		||||
    /// Debug logger
 | 
			
		||||
    let debug (ctx : HttpContext) message =
 | 
			
		||||
        let fac = ctx.GetService<ILoggerFactory> ()
 | 
			
		||||
        let fac = ctx.GetService<ILoggerFactory>()
 | 
			
		||||
        let log = fac.CreateLogger "Debug"
 | 
			
		||||
        log.LogInformation message
 | 
			
		||||
 | 
			
		||||
@ -115,7 +115,7 @@ module private Helpers =
 | 
			
		||||
    let renderComponent nodes : HttpHandler =
 | 
			
		||||
        noResponseCaching
 | 
			
		||||
        >=> fun _ ctx -> backgroundTask {
 | 
			
		||||
            return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
 | 
			
		||||
            return! ctx.WriteHtmlStringAsync(ViewEngine.RenderView.AsString.htmlNodes nodes)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    open Views.Layout
 | 
			
		||||
@ -125,7 +125,7 @@ module private Helpers =
 | 
			
		||||
    let pageContext (ctx : HttpContext) pageTitle content = backgroundTask {
 | 
			
		||||
        let! hasSnoozed =
 | 
			
		||||
            match ctx.CurrentUser with
 | 
			
		||||
            | Some _ -> Journal.hasSnoozed ctx.UserId (ctx.Now ())
 | 
			
		||||
            | Some _ -> Journal.hasSnoozed ctx.UserId (ctx.Now())
 | 
			
		||||
            | None   -> Task.FromResult false
 | 
			
		||||
        return
 | 
			
		||||
            {   IsAuthenticated = Option.isSome ctx.CurrentUser
 | 
			
		||||
@ -153,7 +153,7 @@ module private Helpers =
 | 
			
		||||
 | 
			
		||||
        /// Push a new message into the list
 | 
			
		||||
        let push (ctx : HttpContext) message url = lock upd8 (fun () ->
 | 
			
		||||
            messages <- messages.Add (ctx.UserId, (message, url)))
 | 
			
		||||
            messages <- messages.Add(ctx.UserId, (message, url)))
 | 
			
		||||
 | 
			
		||||
        /// Add a success message header to the response
 | 
			
		||||
        let pushSuccess ctx message url =
 | 
			
		||||
@ -259,7 +259,7 @@ module Components =
 | 
			
		||||
    // GET /components/request-item/[req-id]
 | 
			
		||||
    let requestItem reqId : HttpHandler = requireUser >=> fun next ctx -> task {
 | 
			
		||||
        match! Journal.tryById (RequestId.ofString reqId) ctx.UserId with
 | 
			
		||||
        | Some req -> return! renderComponent [ Views.Request.reqListItem (ctx.Now ()) ctx.TimeZone req ] next ctx
 | 
			
		||||
        | Some req -> return! renderComponent [ Views.Request.reqListItem (ctx.Now()) ctx.TimeZone req ] next ctx
 | 
			
		||||
        | None     -> return! Error.notFound next ctx
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -270,7 +270,7 @@ module Components =
 | 
			
		||||
    // GET /components/request/[req-id]/notes
 | 
			
		||||
    let notes requestId : HttpHandler = requireUser >=> fun next ctx -> task {
 | 
			
		||||
        let! notes = Note.byRequestId (RequestId.ofString requestId) ctx.UserId
 | 
			
		||||
        return! renderComponent (Views.Request.notes (ctx.Now ()) ctx.TimeZone notes) next ctx
 | 
			
		||||
        return! renderComponent (Views.Request.notes (ctx.Now()) ctx.TimeZone notes) next ctx
 | 
			
		||||
    }
 | 
			
		||||
  
 | 
			
		||||
    // GET /components/request/[req-id]/snooze
 | 
			
		||||
@ -294,7 +294,7 @@ module Journal =
 | 
			
		||||
        let usr =
 | 
			
		||||
            ctx.User.Claims
 | 
			
		||||
            |> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName)
 | 
			
		||||
            |> Option.map (fun c -> c.Value)
 | 
			
		||||
            |> Option.map (_.Value)
 | 
			
		||||
            |> Option.defaultValue "Your"
 | 
			
		||||
        let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's"
 | 
			
		||||
        return! partial $"{title} Prayer Journal" (Views.Journal.journal usr) next ctx
 | 
			
		||||
@ -362,8 +362,8 @@ module Request =
 | 
			
		||||
        let reqId  = RequestId.ofString requestId
 | 
			
		||||
        match! Request.existsById reqId userId with
 | 
			
		||||
        | true ->
 | 
			
		||||
            let! notes = ctx.BindFormAsync<Models.NoteEntry> ()
 | 
			
		||||
            do! Note.add reqId userId { AsOf = ctx.Now (); Notes = notes.notes }
 | 
			
		||||
            let! notes = ctx.BindFormAsync<Models.NoteEntry>()
 | 
			
		||||
            do! Note.add reqId userId { AsOf = ctx.Now(); Notes = notes.notes }
 | 
			
		||||
            return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx
 | 
			
		||||
        | false -> return! Error.notFound next ctx
 | 
			
		||||
    }
 | 
			
		||||
@ -371,13 +371,13 @@ module Request =
 | 
			
		||||
    // GET /requests/active
 | 
			
		||||
    let active : HttpHandler = requireUser >=> fun next ctx -> task {
 | 
			
		||||
        let! reqs = Journal.forUser ctx.UserId
 | 
			
		||||
        return! partial "Active Requests" (Views.Request.active (ctx.Now ()) ctx.TimeZone reqs) next ctx
 | 
			
		||||
        return! partial "Active Requests" (Views.Request.active (ctx.Now()) ctx.TimeZone reqs) next ctx
 | 
			
		||||
    }
 | 
			
		||||
  
 | 
			
		||||
    // GET /requests/snoozed
 | 
			
		||||
    let snoozed : HttpHandler = requireUser >=> fun next ctx -> task {
 | 
			
		||||
        let! reqs    = Journal.forUser ctx.UserId
 | 
			
		||||
        let  now     = ctx.Now ()
 | 
			
		||||
        let  now     = ctx.Now()
 | 
			
		||||
        let  snoozed = reqs
 | 
			
		||||
                       |> List.filter (fun it -> defaultArg (it.SnoozedUntil |> Option.map (fun it -> it > now)) false)
 | 
			
		||||
        return! partial "Snoozed Requests" (Views.Request.snoozed now ctx.TimeZone snoozed) next ctx
 | 
			
		||||
@ -386,7 +386,7 @@ module Request =
 | 
			
		||||
    // GET /requests/answered
 | 
			
		||||
    let answered : HttpHandler = requireUser >=> fun next ctx -> task {
 | 
			
		||||
        let! reqs = Journal.answered ctx.UserId
 | 
			
		||||
        return! partial "Answered Requests" (Views.Request.answered (ctx.Now ()) ctx.TimeZone reqs) next ctx
 | 
			
		||||
        return! partial "Answered Requests" (Views.Request.answered (ctx.Now()) ctx.TimeZone reqs) next ctx
 | 
			
		||||
    }
 | 
			
		||||
  
 | 
			
		||||
    // GET /request/[req-id]/full
 | 
			
		||||
@ -413,11 +413,11 @@ module Request =
 | 
			
		||||
        let reqId  = RequestId.ofString requestId
 | 
			
		||||
        match! Request.existsById reqId userId with
 | 
			
		||||
        | true ->
 | 
			
		||||
            let! until = ctx.BindFormAsync<Models.SnoozeUntil> ()
 | 
			
		||||
            let! until = ctx.BindFormAsync<Models.SnoozeUntil>()
 | 
			
		||||
            let date =
 | 
			
		||||
                LocalDatePattern.CreateWithInvariantCulture("yyyy-MM-dd").Parse(until.until).Value
 | 
			
		||||
                    .AtStartOfDayInZone(DateTimeZone.Utc)
 | 
			
		||||
                    .ToInstant ()
 | 
			
		||||
                    .ToInstant()
 | 
			
		||||
            do! Request.updateSnoozed reqId userId (Some date)
 | 
			
		||||
            return!
 | 
			
		||||
                (withSuccessMessage $"Request snoozed until {until.until}"
 | 
			
		||||
@ -444,9 +444,9 @@ module Request =
 | 
			
		||||
 | 
			
		||||
    // POST /request
 | 
			
		||||
    let add : HttpHandler = requireUser >=> fun next ctx -> task {
 | 
			
		||||
        let! form   = ctx.BindModelAsync<Models.Request> ()
 | 
			
		||||
        let! form   = ctx.BindModelAsync<Models.Request>()
 | 
			
		||||
        let  userId = ctx.UserId
 | 
			
		||||
        let  now    = ctx.Now ()
 | 
			
		||||
        let  now    = ctx.Now()
 | 
			
		||||
        let  req    =
 | 
			
		||||
            { Request.empty with
 | 
			
		||||
                Id         = Cuid.generate () |> RequestId
 | 
			
		||||
@ -468,7 +468,7 @@ module Request =
 | 
			
		||||
  
 | 
			
		||||
    // PATCH /request
 | 
			
		||||
    let update : HttpHandler = requireUser >=> fun next ctx -> task {
 | 
			
		||||
        let! form   = ctx.BindModelAsync<Models.Request> ()
 | 
			
		||||
        let! form   = ctx.BindModelAsync<Models.Request>()
 | 
			
		||||
        let  userId = ctx.UserId
 | 
			
		||||
        // TODO: update the instance and save rather than all these little updates
 | 
			
		||||
        match! Journal.tryById (RequestId.ofString form.requestId) userId with
 | 
			
		||||
@ -483,10 +483,10 @@ module Request =
 | 
			
		||||
                | Immediate -> do! Request.updateShowAfter req.RequestId userId None
 | 
			
		||||
                | _         -> ()
 | 
			
		||||
            // append history
 | 
			
		||||
            let upd8Text = form.requestText.Trim ()
 | 
			
		||||
            let upd8Text = form.requestText.Trim()
 | 
			
		||||
            let text     = if upd8Text = req.Text then None else Some upd8Text
 | 
			
		||||
            do! History.add req.RequestId userId
 | 
			
		||||
                    { AsOf = ctx.Now (); Status = (Option.get >> RequestAction.ofString) form.status; Text = text }
 | 
			
		||||
                    { AsOf = ctx.Now(); Status = (Option.get >> RequestAction.ofString) form.status; Text = text }
 | 
			
		||||
            let nextUrl =
 | 
			
		||||
                match form.returnTo with
 | 
			
		||||
                | "active"          -> "/requests/active"
 | 
			
		||||
@ -510,7 +510,7 @@ module User =
 | 
			
		||||
  
 | 
			
		||||
    // GET /user/log-off
 | 
			
		||||
    let logOff : HttpHandler = requireUser >=> fun next ctx -> task {
 | 
			
		||||
        do! ctx.SignOutAsync ("Auth0", AuthenticationProperties (RedirectUri = "/"))
 | 
			
		||||
        do! ctx.SignOutAsync("Auth0", AuthenticationProperties (RedirectUri = "/"))
 | 
			
		||||
        do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
 | 
			
		||||
        return! next ctx
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk.Web">
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>net7.0</TargetFramework>
 | 
			
		||||
    <Version>3.3</Version>
 | 
			
		||||
    <TargetFramework>net8.0</TargetFramework>
 | 
			
		||||
    <Version>3.4</Version>
 | 
			
		||||
    <DebugType>embedded</DebugType>
 | 
			
		||||
    <GenerateDocumentationFile>false</GenerateDocumentationFile>
 | 
			
		||||
    <PublishSingleFile>false</PublishSingleFile>
 | 
			
		||||
@ -20,16 +20,15 @@
 | 
			
		||||
    <Compile Include="Program.fs" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="BitBadger.Npgsql.FSharp.Documents" Version="1.0.0-beta3" />
 | 
			
		||||
    <PackageReference Include="BitBadger.Npgsql.FSharp.Documents" Version="2.0.0" />
 | 
			
		||||
    <PackageReference Include="FSharp.SystemTextJson" Version="1.2.42" />
 | 
			
		||||
    <PackageReference Include="FunctionalCuid" Version="1.0.0" />
 | 
			
		||||
    <PackageReference Include="Giraffe" Version="6.2.0" />
 | 
			
		||||
    <PackageReference Include="Giraffe.Htmx" Version="1.9.6" />
 | 
			
		||||
    <PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.9.6" />
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.11" />
 | 
			
		||||
    <PackageReference Include="Giraffe.Htmx" Version="1.9.8" />
 | 
			
		||||
    <PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.9.8" />
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" />
 | 
			
		||||
    <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.1.2" />
 | 
			
		||||
    <PackageReference Include="Npgsql.NodaTime" Version="7.0.6" />
 | 
			
		||||
    <PackageReference Update="FSharp.Core" Version="7.0.400" />
 | 
			
		||||
    <PackageReference Include="Npgsql.NodaTime" Version="8.0.1" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <Folder Include="wwwroot\" />
 | 
			
		||||
 | 
			
		||||
@ -31,10 +31,10 @@ let main args =
 | 
			
		||||
    let builder = WebApplication.CreateBuilder args
 | 
			
		||||
    let _       = builder.Configuration.AddEnvironmentVariables "MPJ_"
 | 
			
		||||
    let svc     = builder.Services
 | 
			
		||||
    let cfg     = svc.BuildServiceProvider().GetRequiredService<IConfiguration> ()
 | 
			
		||||
    let cfg     = svc.BuildServiceProvider().GetRequiredService<IConfiguration>()
 | 
			
		||||
 | 
			
		||||
    let _ = svc.AddRouting ()
 | 
			
		||||
    let _ = svc.AddGiraffe ()
 | 
			
		||||
    let _ = svc.AddRouting()
 | 
			
		||||
    let _ = svc.AddGiraffe()
 | 
			
		||||
    let _ = svc.AddSingleton<IClock> SystemClock.Instance
 | 
			
		||||
    let _ = svc.AddSingleton<IDateTimeZoneProvider> DateTimeZoneProviders.Tzdb
 | 
			
		||||
    let _ = svc.Configure<ForwardedHeadersOptions>(fun (opts : ForwardedHeadersOptions) ->
 | 
			
		||||
@ -59,7 +59,7 @@ let main args =
 | 
			
		||||
                opts.ClientSecret <- auth0["Secret"]
 | 
			
		||||
                opts.ResponseType <- OpenIdConnectResponseType.Code
 | 
			
		||||
                
 | 
			
		||||
                opts.Scope.Clear ()
 | 
			
		||||
                opts.Scope.Clear()
 | 
			
		||||
                opts.Scope.Add "openid"
 | 
			
		||||
                opts.Scope.Add "profile"
 | 
			
		||||
                
 | 
			
		||||
@ -67,7 +67,7 @@ let main args =
 | 
			
		||||
                opts.ClaimsIssuer <- "Auth0"
 | 
			
		||||
                opts.SaveTokens   <- true
 | 
			
		||||
                
 | 
			
		||||
                opts.Events <- OpenIdConnectEvents ()
 | 
			
		||||
                opts.Events <- OpenIdConnectEvents()
 | 
			
		||||
                opts.Events.OnRedirectToIdentityProviderForSignOut <- fun ctx ->
 | 
			
		||||
                    let returnTo =
 | 
			
		||||
                        match ctx.Properties.RedirectUri with
 | 
			
		||||
@ -82,7 +82,7 @@ let main args =
 | 
			
		||||
                                | false -> redirUri
 | 
			
		||||
                            Uri.EscapeDataString $"&returnTo={finalRedirUri}"
 | 
			
		||||
                    ctx.Response.Redirect $"""https://{auth0["Domain"]}/v2/logout?client_id={auth0["Id"]}{returnTo}"""
 | 
			
		||||
                    ctx.HandleResponse ()
 | 
			
		||||
                    ctx.HandleResponse()
 | 
			
		||||
                    Task.CompletedTask
 | 
			
		||||
                opts.Events.OnRedirectToIdentityProvider <- fun ctx ->
 | 
			
		||||
                    let uri = UriBuilder ctx.ProtocolMessage.RedirectUri
 | 
			
		||||
@ -92,20 +92,20 @@ let main args =
 | 
			
		||||
                    Task.CompletedTask)
 | 
			
		||||
    
 | 
			
		||||
    let _ = svc.AddSingleton<JsonSerializerOptions> Json.options
 | 
			
		||||
    let _ = svc.AddSingleton<Json.ISerializer> (SystemTextJson.Serializer Json.options)
 | 
			
		||||
    let _ = svc.AddSingleton<Json.ISerializer>(SystemTextJson.Serializer Json.options)
 | 
			
		||||
    let _ = Connection.setUp cfg |> Async.AwaitTask |> Async.RunSynchronously
 | 
			
		||||
    
 | 
			
		||||
    if builder.Environment.IsDevelopment () then builder.Logging.AddFilter (fun l -> l > LogLevel.Information) |> ignore
 | 
			
		||||
    if builder.Environment.IsDevelopment() then builder.Logging.AddFilter(fun l -> l > LogLevel.Information) |> ignore
 | 
			
		||||
    let _ = builder.Logging.AddConsole().AddDebug() |> ignore
 | 
			
		||||
 | 
			
		||||
    use app = builder.Build ()
 | 
			
		||||
    let _ = app.UseStaticFiles ()
 | 
			
		||||
    let _ = app.UseCookiePolicy ()
 | 
			
		||||
    let _ = app.UseRouting ()
 | 
			
		||||
    let _ = app.UseAuthentication ()
 | 
			
		||||
    use app = builder.Build()
 | 
			
		||||
    let _ = app.UseStaticFiles()
 | 
			
		||||
    let _ = app.UseCookiePolicy()
 | 
			
		||||
    let _ = app.UseRouting()
 | 
			
		||||
    let _ = app.UseAuthentication()
 | 
			
		||||
    let _ = app.UseGiraffeErrorHandler Handlers.Error.error
 | 
			
		||||
    let _ = app.UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes)
 | 
			
		||||
    let _ = app.UseEndpoints(fun e -> e.MapGiraffeEndpoints Handlers.routes)
 | 
			
		||||
    
 | 
			
		||||
    app.Run ()
 | 
			
		||||
    app.Run()
 | 
			
		||||
 | 
			
		||||
    0
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ let noResults heading link buttonText text =
 | 
			
		||||
 | 
			
		||||
/// Create a date with a span tag, displaying the relative date with the full date/time in the tooltip
 | 
			
		||||
let relativeDate (date : Instant) now (tz : DateTimeZone) =
 | 
			
		||||
    span [ _title (date.InZone(tz).ToDateTimeOffset().ToString ("f", null)) ] [ Dates.formatDistance now date |> str ]
 | 
			
		||||
    span [ _title (date.InZone(tz).ToDateTimeOffset().ToString("f", null)) ] [ Dates.formatDistance now date |> str ]
 | 
			
		||||
 | 
			
		||||
/// The version of myPrayerJournal
 | 
			
		||||
let version =
 | 
			
		||||
 | 
			
		||||
@ -74,13 +74,13 @@ let snoozed now tz reqs =
 | 
			
		||||
 | 
			
		||||
/// View for Full Request page
 | 
			
		||||
let full (clock : IClock) tz (req : Request) =
 | 
			
		||||
    let now = clock.GetCurrentInstant ()
 | 
			
		||||
    let now = clock.GetCurrentInstant()
 | 
			
		||||
    let answered =
 | 
			
		||||
        req.History
 | 
			
		||||
        |> Seq.ofList
 | 
			
		||||
        |> Seq.filter History.isAnswered
 | 
			
		||||
        |> Seq.tryHead
 | 
			
		||||
        |> Option.map (fun x -> x.AsOf)
 | 
			
		||||
        |> Option.map (_.AsOf)
 | 
			
		||||
    let prayed = (req.History |> List.filter History.isPrayed |> List.length).ToString "N0"
 | 
			
		||||
    let daysOpen =
 | 
			
		||||
        let asOf = defaultArg answered now
 | 
			
		||||
@ -89,7 +89,7 @@ let full (clock : IClock) tz (req : Request) =
 | 
			
		||||
        req.History
 | 
			
		||||
        |> Seq.ofList
 | 
			
		||||
        |> Seq.filter (fun h -> Option.isSome h.Text)
 | 
			
		||||
        |> Seq.sortByDescending (fun h -> h.AsOf)
 | 
			
		||||
        |> Seq.sortByDescending (_.AsOf)
 | 
			
		||||
        |> Seq.map (fun h -> Option.get h.Text)
 | 
			
		||||
        |> Seq.head
 | 
			
		||||
    // The history log including notes (and excluding the final entry for answered requests)
 | 
			
		||||
@ -100,7 +100,7 @@ let full (clock : IClock) tz (req : Request) =
 | 
			
		||||
            |> Seq.ofList
 | 
			
		||||
            |> Seq.map (fun n -> {| asOf = n.AsOf; text = Some n.Notes; status = "Notes" |})
 | 
			
		||||
            |> Seq.append (req.History |> List.map toDisp)
 | 
			
		||||
            |> Seq.sortByDescending (fun it -> it.asOf)
 | 
			
		||||
            |> Seq.sortByDescending (_.asOf)
 | 
			
		||||
            |> List.ofSeq
 | 
			
		||||
        // Skip the first entry for answered requests; that info is already displayed
 | 
			
		||||
        match answered with Some _ -> all.Tail | None -> all
 | 
			
		||||
@ -112,7 +112,7 @@ let full (clock : IClock) tz (req : Request) =
 | 
			
		||||
                    match answered with
 | 
			
		||||
                    | Some date ->
 | 
			
		||||
                        str "Answered "
 | 
			
		||||
                        date.ToDateTimeOffset().ToString ("D", null) |> str
 | 
			
		||||
                        date.ToDateTimeOffset().ToString("D", null) |> str
 | 
			
		||||
                        str " ("
 | 
			
		||||
                        relativeDate date now tz
 | 
			
		||||
                        rawText ") • "
 | 
			
		||||
@ -127,7 +127,7 @@ let full (clock : IClock) tz (req : Request) =
 | 
			
		||||
                    p [ _class "m-0" ] [
 | 
			
		||||
                        str it.status
 | 
			
		||||
                        rawText "  "
 | 
			
		||||
                        small [] [ em [] [ it.asOf.ToDateTimeOffset().ToString ("D", null) |> str ] ]
 | 
			
		||||
                        small [] [ em [] [ it.asOf.ToDateTimeOffset().ToString("D", null) |> str ] ]
 | 
			
		||||
                    ]
 | 
			
		||||
                    match it.text with
 | 
			
		||||
                    | Some txt -> p [ _class "mt-2 mb-0" ] [ str txt ]
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user