2018-08-07 02:21:28 +00:00
|
|
|
/// HTTP handlers for the myPrayerJournal API
|
|
|
|
[<RequireQualifiedAccess>]
|
2019-08-01 00:13:36 +00:00
|
|
|
module MyPrayerJournal.Handlers
|
2018-08-07 02:21:28 +00:00
|
|
|
|
|
|
|
open Giraffe
|
|
|
|
|
2018-08-15 02:01:21 +00:00
|
|
|
/// Handler to return Vue files
|
|
|
|
module Vue =
|
|
|
|
|
|
|
|
/// The application index page
|
|
|
|
let app : HttpHandler = htmlFile "wwwroot/index.html"
|
|
|
|
|
2019-08-09 22:00:09 +00:00
|
|
|
open System
|
2018-08-15 02:01:21 +00:00
|
|
|
|
|
|
|
/// Handlers for error conditions
|
2018-08-07 02:21:28 +00:00
|
|
|
module Error =
|
|
|
|
|
|
|
|
open Microsoft.Extensions.Logging
|
|
|
|
|
|
|
|
/// 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 404s from the API, sending known URL paths to the Vue app so that they can be handled there
|
|
|
|
let notFound : HttpHandler =
|
|
|
|
fun next ctx ->
|
2018-08-18 03:12:14 +00:00
|
|
|
[ "/journal"; "/legal"; "/request"; "/user" ]
|
2018-08-07 02:21:28 +00:00
|
|
|
|> List.filter ctx.Request.Path.Value.StartsWith
|
|
|
|
|> List.length
|
|
|
|
|> function
|
|
|
|
| 0 -> (setStatusCode 404 >=> json ([ "error", "not found" ] |> dict)) next ctx
|
2018-08-15 02:01:21 +00:00
|
|
|
| _ -> Vue.app next ctx
|
2018-08-07 02:21:28 +00:00
|
|
|
|
2019-08-09 22:00:09 +00:00
|
|
|
open Cuid
|
2018-08-07 02:21:28 +00:00
|
|
|
|
|
|
|
/// Handler helpers
|
|
|
|
[<AutoOpen>]
|
|
|
|
module private Helpers =
|
|
|
|
|
|
|
|
open Microsoft.AspNetCore.Http
|
2019-07-14 03:55:53 +00:00
|
|
|
open Raven.Client.Documents
|
2018-08-07 02:21:28 +00:00
|
|
|
open System.Threading.Tasks
|
|
|
|
open System.Security.Claims
|
|
|
|
|
2019-07-14 03:55:53 +00:00
|
|
|
/// Create a RavenDB session
|
|
|
|
let session (ctx : HttpContext) =
|
2019-08-01 00:13:36 +00:00
|
|
|
let sess = ctx.GetService<IDocumentStore>().OpenAsyncSession ()
|
|
|
|
sess.Advanced.WaitForIndexesAfterSaveChanges ()
|
|
|
|
sess
|
2019-07-14 03:55:53 +00:00
|
|
|
|
2018-08-07 02:21:28 +00:00
|
|
|
/// Get the user's "sub" claim
|
|
|
|
let user (ctx : HttpContext) =
|
|
|
|
ctx.User.Claims |> Seq.tryFind (fun u -> u.Type = ClaimTypes.NameIdentifier)
|
|
|
|
|
|
|
|
/// Get the current user's ID
|
|
|
|
// NOTE: this may raise if you don't run the request through the authorize handler first
|
|
|
|
let userId ctx =
|
2019-07-14 03:55:53 +00:00
|
|
|
((user >> Option.get) ctx).Value |> UserId
|
2018-08-07 02:21:28 +00:00
|
|
|
|
2019-07-15 01:47:31 +00:00
|
|
|
/// Create a request ID from a string
|
2019-08-09 22:00:09 +00:00
|
|
|
let toReqId x =
|
|
|
|
let reqId =
|
|
|
|
match Cuid.ofString x with
|
|
|
|
| Ok cuid -> cuid
|
|
|
|
| Error msg -> invalidOp msg
|
|
|
|
RequestId reqId
|
2019-07-15 01:47:31 +00:00
|
|
|
|
2018-08-07 02:21:28 +00:00
|
|
|
/// Return a 201 CREATED response
|
|
|
|
let created next ctx =
|
|
|
|
setStatusCode 201 next ctx
|
|
|
|
|
2019-07-15 01:47:31 +00:00
|
|
|
/// The "now" time in JavaScript as Ticks
|
2018-08-07 02:21:28 +00:00
|
|
|
let jsNow () =
|
2019-07-15 01:47:31 +00:00
|
|
|
(int64 >> (*) 1000L >> Ticks) <| DateTime.UtcNow.Subtract(DateTime (1970, 1, 1, 0, 0, 0)).TotalSeconds
|
2018-08-07 02:21:28 +00:00
|
|
|
|
|
|
|
/// Handler to return a 403 Not Authorized reponse
|
|
|
|
let notAuthorized : HttpHandler =
|
|
|
|
setStatusCode 403 >=> fun _ _ -> Task.FromResult<HttpContext option> None
|
|
|
|
|
|
|
|
/// Handler to require authorization
|
|
|
|
let authorize : HttpHandler =
|
|
|
|
fun next ctx -> match user ctx with Some _ -> next ctx | None -> notAuthorized next ctx
|
|
|
|
|
|
|
|
/// Flip JSON result so we can pipe into it
|
|
|
|
let asJson<'T> next ctx (o : 'T) =
|
|
|
|
json o next ctx
|
|
|
|
|
|
|
|
|
|
|
|
/// Strongly-typed models for post requests
|
|
|
|
module Models =
|
|
|
|
|
|
|
|
/// A history entry addition (AKA request update)
|
|
|
|
[<CLIMutable>]
|
|
|
|
type HistoryEntry =
|
|
|
|
{ /// The status of the history update
|
|
|
|
status : string
|
|
|
|
/// The text of the update
|
|
|
|
updateText : string
|
|
|
|
}
|
|
|
|
|
|
|
|
/// An additional note
|
|
|
|
[<CLIMutable>]
|
|
|
|
type NoteEntry =
|
|
|
|
{ /// The notes being added
|
|
|
|
notes : string
|
|
|
|
}
|
|
|
|
|
2018-08-19 00:32:48 +00:00
|
|
|
/// Recurrence update
|
|
|
|
[<CLIMutable>]
|
|
|
|
type Recurrence =
|
|
|
|
{ /// The recurrence type
|
|
|
|
recurType : string
|
|
|
|
/// The recurrence cound
|
|
|
|
recurCount : int16
|
|
|
|
}
|
|
|
|
|
2018-08-07 02:21:28 +00:00
|
|
|
/// A prayer request
|
|
|
|
[<CLIMutable>]
|
|
|
|
type Request =
|
|
|
|
{ /// The text of the request
|
|
|
|
requestText : string
|
2018-08-15 02:01:21 +00:00
|
|
|
/// The recurrence type
|
|
|
|
recurType : string
|
|
|
|
/// The recurrence count
|
2018-08-19 00:32:48 +00:00
|
|
|
recurCount : int16
|
2018-08-07 02:21:28 +00:00
|
|
|
}
|
|
|
|
|
2018-08-19 00:32:48 +00:00
|
|
|
/// Reset the "showAfter" property on a request
|
|
|
|
[<CLIMutable>]
|
|
|
|
type Show =
|
|
|
|
{ /// The time after which the request should appear
|
|
|
|
showAfter : int64
|
|
|
|
}
|
|
|
|
|
2018-08-07 02:21:28 +00:00
|
|
|
/// The time until which a request should not appear in the journal
|
|
|
|
[<CLIMutable>]
|
|
|
|
type SnoozeUntil =
|
|
|
|
{ /// The time at which the request should reappear
|
|
|
|
until : int64
|
|
|
|
}
|
|
|
|
|
2019-08-09 22:00:09 +00:00
|
|
|
open FSharp.Control.Tasks.V2.ContextInsensitive
|
|
|
|
|
2018-08-07 02:21:28 +00:00
|
|
|
/// /api/journal URLs
|
|
|
|
module Journal =
|
|
|
|
|
|
|
|
/// GET /api/journal
|
|
|
|
let journal : HttpHandler =
|
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
2019-07-15 01:47:31 +00:00
|
|
|
task {
|
2019-07-31 04:56:34 +00:00
|
|
|
use sess = session ctx
|
|
|
|
let usrId = userId ctx
|
|
|
|
let! jrnl = Data.journalByUserId usrId sess
|
2019-07-15 01:47:31 +00:00
|
|
|
return! json jrnl next ctx
|
|
|
|
}
|
2018-08-07 02:21:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
/// /api/request URLs
|
|
|
|
module Request =
|
|
|
|
|
|
|
|
/// POST /api/request
|
|
|
|
let add : HttpHandler =
|
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
|
|
|
task {
|
|
|
|
let! r = ctx.BindJsonAsync<Models.Request> ()
|
2019-07-14 03:55:53 +00:00
|
|
|
use sess = session ctx
|
2019-08-09 22:00:09 +00:00
|
|
|
let reqId = (Cuid.generate >> RequestId) ()
|
2018-08-07 02:21:28 +00:00
|
|
|
let usrId = userId ctx
|
2019-07-15 01:47:31 +00:00
|
|
|
let now = jsNow ()
|
2019-07-31 04:56:34 +00:00
|
|
|
do! Data.addRequest
|
2019-07-14 03:55:53 +00:00
|
|
|
{ Request.empty with
|
2019-07-28 01:02:01 +00:00
|
|
|
Id = RequestId.toString reqId
|
2019-07-14 03:55:53 +00:00
|
|
|
userId = usrId
|
|
|
|
enteredOn = now
|
|
|
|
showAfter = now
|
|
|
|
recurType = Recurrence.fromString r.recurType
|
|
|
|
recurCount = r.recurCount
|
|
|
|
history = [
|
2019-07-31 04:56:34 +00:00
|
|
|
{ asOf = now
|
|
|
|
status = Created
|
|
|
|
text = Some r.requestText
|
2019-07-14 03:55:53 +00:00
|
|
|
}
|
|
|
|
]
|
2019-07-31 04:56:34 +00:00
|
|
|
} sess
|
|
|
|
do! Data.saveChanges sess
|
|
|
|
match! Data.tryJournalById reqId usrId sess with
|
2018-08-15 02:01:21 +00:00
|
|
|
| Some req -> return! (setStatusCode 201 >=> json req) next ctx
|
2018-08-07 02:21:28 +00:00
|
|
|
| None -> return! Error.notFound next ctx
|
|
|
|
}
|
|
|
|
|
|
|
|
/// POST /api/request/[req-id]/history
|
2019-07-15 01:47:31 +00:00
|
|
|
let addHistory requestId : HttpHandler =
|
2018-08-07 02:21:28 +00:00
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
|
|
|
task {
|
2019-07-14 03:55:53 +00:00
|
|
|
use sess = session ctx
|
2019-07-31 04:56:34 +00:00
|
|
|
let usrId = userId ctx
|
2019-07-15 01:47:31 +00:00
|
|
|
let reqId = toReqId requestId
|
2019-07-31 04:56:34 +00:00
|
|
|
match! Data.tryRequestById reqId usrId sess with
|
2018-08-15 02:01:21 +00:00
|
|
|
| Some req ->
|
2018-08-07 02:21:28 +00:00
|
|
|
let! hist = ctx.BindJsonAsync<Models.HistoryEntry> ()
|
2019-07-15 01:47:31 +00:00
|
|
|
let now = jsNow ()
|
2019-07-29 01:47:11 +00:00
|
|
|
let act = RequestAction.fromString hist.status
|
2019-07-31 04:56:34 +00:00
|
|
|
Data.addHistory reqId
|
|
|
|
{ asOf = now
|
2019-07-29 01:47:11 +00:00
|
|
|
status = act
|
2019-07-14 03:55:53 +00:00
|
|
|
text = match hist.updateText with null | "" -> None | x -> Some x
|
2019-07-31 04:56:34 +00:00
|
|
|
} sess
|
2019-07-29 01:47:11 +00:00
|
|
|
match act with
|
|
|
|
| Prayed ->
|
2019-07-31 04:56:34 +00:00
|
|
|
let nextShow = (Ticks.toLong now) + (Recurrence.duration req.recurType * int64 req.recurCount)
|
|
|
|
Data.updateShowAfter reqId (Ticks nextShow) sess
|
2018-08-15 02:01:21 +00:00
|
|
|
| _ -> ()
|
2019-07-31 04:56:34 +00:00
|
|
|
do! Data.saveChanges sess
|
2018-08-07 02:21:28 +00:00
|
|
|
return! created next ctx
|
|
|
|
| None -> return! Error.notFound next ctx
|
|
|
|
}
|
|
|
|
|
|
|
|
/// POST /api/request/[req-id]/note
|
2019-07-15 01:47:31 +00:00
|
|
|
let addNote requestId : HttpHandler =
|
2018-08-07 02:21:28 +00:00
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
|
|
|
task {
|
2019-07-15 01:47:31 +00:00
|
|
|
use sess = session ctx
|
2019-07-31 04:56:34 +00:00
|
|
|
let usrId = userId ctx
|
2019-07-15 01:47:31 +00:00
|
|
|
let reqId = toReqId requestId
|
2019-07-31 04:56:34 +00:00
|
|
|
match! Data.tryRequestById reqId usrId sess with
|
2018-08-07 02:21:28 +00:00
|
|
|
| Some _ ->
|
|
|
|
let! notes = ctx.BindJsonAsync<Models.NoteEntry> ()
|
2019-07-31 04:56:34 +00:00
|
|
|
Data.addNote reqId { asOf = jsNow (); notes = notes.notes } sess
|
|
|
|
do! Data.saveChanges sess
|
2018-08-07 02:21:28 +00:00
|
|
|
return! created next ctx
|
|
|
|
| None -> return! Error.notFound next ctx
|
|
|
|
}
|
|
|
|
|
|
|
|
/// GET /api/requests/answered
|
|
|
|
let answered : HttpHandler =
|
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
2019-07-15 01:47:31 +00:00
|
|
|
task {
|
2019-07-31 04:56:34 +00:00
|
|
|
use sess = session ctx
|
|
|
|
let usrId = userId ctx
|
|
|
|
let! reqs = Data.answeredRequests usrId sess
|
2019-07-15 01:47:31 +00:00
|
|
|
return! json reqs next ctx
|
|
|
|
}
|
2018-08-07 02:21:28 +00:00
|
|
|
|
|
|
|
/// GET /api/request/[req-id]
|
2019-07-15 01:47:31 +00:00
|
|
|
let get requestId : HttpHandler =
|
2018-08-07 02:21:28 +00:00
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
|
|
|
task {
|
2019-07-31 04:56:34 +00:00
|
|
|
use sess = session ctx
|
|
|
|
let usrId = userId ctx
|
|
|
|
match! Data.tryJournalById (toReqId requestId) usrId sess with
|
2018-08-15 02:01:21 +00:00
|
|
|
| Some req -> return! json req next ctx
|
2018-08-07 02:21:28 +00:00
|
|
|
| None -> return! Error.notFound next ctx
|
|
|
|
}
|
|
|
|
|
|
|
|
/// GET /api/request/[req-id]/full
|
2019-07-15 01:47:31 +00:00
|
|
|
let getFull requestId : HttpHandler =
|
2018-08-07 02:21:28 +00:00
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
|
|
|
task {
|
2019-07-31 04:56:34 +00:00
|
|
|
use sess = session ctx
|
|
|
|
let usrId = userId ctx
|
|
|
|
match! Data.tryFullRequestById (toReqId requestId) usrId sess with
|
2018-08-15 02:01:21 +00:00
|
|
|
| Some req -> return! json req next ctx
|
2018-08-07 02:21:28 +00:00
|
|
|
| None -> return! Error.notFound next ctx
|
|
|
|
}
|
|
|
|
|
|
|
|
/// GET /api/request/[req-id]/notes
|
2019-07-15 01:47:31 +00:00
|
|
|
let getNotes requestId : HttpHandler =
|
2018-08-07 02:21:28 +00:00
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
|
|
|
task {
|
2019-07-31 04:56:34 +00:00
|
|
|
use sess = session ctx
|
|
|
|
let usrId = userId ctx
|
|
|
|
let! notes = Data.notesById (toReqId requestId) usrId sess
|
2018-08-07 02:21:28 +00:00
|
|
|
return! json notes next ctx
|
|
|
|
}
|
|
|
|
|
2018-08-19 00:32:48 +00:00
|
|
|
/// PATCH /api/request/[req-id]/show
|
2019-07-15 01:47:31 +00:00
|
|
|
let show requestId : HttpHandler =
|
2018-08-19 00:32:48 +00:00
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
|
|
|
task {
|
2019-07-15 01:47:31 +00:00
|
|
|
use sess = session ctx
|
2019-07-31 04:56:34 +00:00
|
|
|
let usrId = userId ctx
|
2019-07-15 01:47:31 +00:00
|
|
|
let reqId = toReqId requestId
|
2019-07-31 04:56:34 +00:00
|
|
|
match! Data.tryRequestById reqId usrId sess with
|
2019-07-15 01:47:31 +00:00
|
|
|
| Some _ ->
|
2018-08-19 00:32:48 +00:00
|
|
|
let! show = ctx.BindJsonAsync<Models.Show> ()
|
2019-07-31 04:56:34 +00:00
|
|
|
Data.updateShowAfter reqId (Ticks show.showAfter) sess
|
|
|
|
do! Data.saveChanges sess
|
2018-08-19 00:32:48 +00:00
|
|
|
return! setStatusCode 204 next ctx
|
|
|
|
| None -> return! Error.notFound next ctx
|
|
|
|
}
|
|
|
|
|
|
|
|
/// PATCH /api/request/[req-id]/snooze
|
2019-07-15 01:47:31 +00:00
|
|
|
let snooze requestId : HttpHandler =
|
2018-08-07 02:21:28 +00:00
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
|
|
|
task {
|
2019-07-15 01:47:31 +00:00
|
|
|
use sess = session ctx
|
2019-07-31 04:56:34 +00:00
|
|
|
let usrId = userId ctx
|
2019-07-15 01:47:31 +00:00
|
|
|
let reqId = toReqId requestId
|
2019-07-31 04:56:34 +00:00
|
|
|
match! Data.tryRequestById reqId usrId sess with
|
2019-07-15 01:47:31 +00:00
|
|
|
| Some _ ->
|
2018-08-07 02:21:28 +00:00
|
|
|
let! until = ctx.BindJsonAsync<Models.SnoozeUntil> ()
|
2019-07-31 04:56:34 +00:00
|
|
|
Data.updateSnoozed reqId (Ticks until.until) sess
|
|
|
|
do! Data.saveChanges sess
|
2018-08-07 02:21:28 +00:00
|
|
|
return! setStatusCode 204 next ctx
|
|
|
|
| None -> return! Error.notFound next ctx
|
|
|
|
}
|
2018-08-19 00:32:48 +00:00
|
|
|
|
|
|
|
/// PATCH /api/request/[req-id]/recurrence
|
2019-07-15 01:47:31 +00:00
|
|
|
let updateRecurrence requestId : HttpHandler =
|
2018-08-19 00:32:48 +00:00
|
|
|
authorize
|
|
|
|
>=> fun next ctx ->
|
|
|
|
task {
|
2019-07-15 01:47:31 +00:00
|
|
|
use sess = session ctx
|
2019-07-31 04:56:34 +00:00
|
|
|
let usrId = userId ctx
|
2019-07-15 01:47:31 +00:00
|
|
|
let reqId = toReqId requestId
|
2019-07-31 04:56:34 +00:00
|
|
|
match! Data.tryRequestById reqId usrId sess with
|
2019-07-15 01:47:31 +00:00
|
|
|
| Some _ ->
|
2018-08-19 00:32:48 +00:00
|
|
|
let! recur = ctx.BindJsonAsync<Models.Recurrence> ()
|
2019-07-31 04:56:34 +00:00
|
|
|
Data.updateRecurrence reqId (Recurrence.fromString recur.recurType) recur.recurCount sess
|
|
|
|
do! Data.saveChanges sess
|
2018-08-19 00:32:48 +00:00
|
|
|
return! setStatusCode 204 next ctx
|
|
|
|
| None -> return! Error.notFound next ctx
|
|
|
|
}
|
2019-08-21 02:41:49 +00:00
|
|
|
|
|
|
|
open Giraffe.TokenRouter
|
|
|
|
|
|
|
|
/// The routes for myPrayerJournal
|
|
|
|
let webApp : HttpHandler =
|
|
|
|
router Error.notFound [
|
|
|
|
route "/" Vue.app
|
|
|
|
subRoute "/api/" [
|
|
|
|
GET [
|
|
|
|
route "journal" Journal.journal
|
|
|
|
subRoute "request" [
|
|
|
|
route "s/answered" Request.answered
|
|
|
|
routef "/%s/full" Request.getFull
|
|
|
|
routef "/%s/notes" Request.getNotes
|
|
|
|
routef "/%s" Request.get
|
|
|
|
]
|
|
|
|
]
|
|
|
|
PATCH [
|
|
|
|
subRoute "request" [
|
|
|
|
routef "/%s/recurrence" Request.updateRecurrence
|
|
|
|
routef "/%s/show" Request.show
|
|
|
|
routef "/%s/snooze" Request.snooze
|
|
|
|
]
|
|
|
|
]
|
|
|
|
POST [
|
|
|
|
subRoute "request" [
|
|
|
|
route "" Request.add
|
|
|
|
routef "/%s/history" Request.addHistory
|
|
|
|
routef "/%s/note" Request.addNote
|
|
|
|
]
|
|
|
|
]
|
|
|
|
]
|
|
|
|
]
|