Files
myPrayerJournal/src/MyPrayerJournal.Api/Handlers.fs
T

321 lines
9.5 KiB
FSharp
Raw Normal View History

2018-08-06 21:21:28 -05:00
/// HTTP handlers for the myPrayerJournal API
[<RequireQualifiedAccess>]
module MyPrayerJournal.Api.Handlers
2019-02-23 10:59:23 -06:00
open FSharp.Control.Tasks.V2.ContextInsensitive
2018-08-06 21:21:28 -05:00
open Giraffe
open MyPrayerJournal
open System
2018-08-14 21:01:21 -05:00
/// Handler to return Vue files
module Vue =
/// The application index page
let app : HttpHandler = htmlFile "wwwroot/index.html"
/// Handlers for error conditions
2018-08-06 21:21:28 -05: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-17 22:12:14 -05:00
[ "/journal"; "/legal"; "/request"; "/user" ]
2018-08-06 21:21:28 -05:00
|> List.filter ctx.Request.Path.Value.StartsWith
|> List.length
|> function
| 0 -> (setStatusCode 404 >=> json ([ "error", "not found" ] |> dict)) next ctx
2018-08-14 21:01:21 -05:00
| _ -> Vue.app next ctx
2018-08-06 21:21:28 -05:00
/// Handler helpers
[<AutoOpen>]
module private Helpers =
open Microsoft.AspNetCore.Http
2019-07-13 22:55:53 -05:00
open Raven.Client.Documents
2018-08-06 21:21:28 -05:00
open System.Threading.Tasks
open System.Security.Claims
2019-07-13 22:55:53 -05:00
/// Create a RavenDB session
let session (ctx : HttpContext) =
ctx.GetService<IDocumentStore>().OpenAsyncSession ()
2018-08-06 21:21:28 -05: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-13 22:55:53 -05:00
((user >> Option.get) ctx).Value |> UserId
2018-08-06 21:21:28 -05:00
2019-07-14 20:47:31 -05:00
/// Create a request ID from a string
2019-07-27 20:02:01 -05:00
let toReqId = Cuid >> RequestId
2019-07-14 20:47:31 -05:00
2018-08-06 21:21:28 -05:00
/// Return a 201 CREATED response
let created next ctx =
setStatusCode 201 next ctx
2019-07-14 20:47:31 -05:00
/// The "now" time in JavaScript as Ticks
2018-08-06 21:21:28 -05:00
let jsNow () =
2019-07-14 20:47:31 -05:00
(int64 >> (*) 1000L >> Ticks) <| DateTime.UtcNow.Subtract(DateTime (1970, 1, 1, 0, 0, 0)).TotalSeconds
2018-08-06 21:21:28 -05: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-18 19:32:48 -05:00
/// Recurrence update
[<CLIMutable>]
type Recurrence =
{ /// The recurrence type
recurType : string
/// The recurrence cound
recurCount : int16
}
2018-08-06 21:21:28 -05:00
/// A prayer request
[<CLIMutable>]
type Request =
{ /// The text of the request
requestText : string
2018-08-14 21:01:21 -05:00
/// The recurrence type
recurType : string
/// The recurrence count
2018-08-18 19:32:48 -05:00
recurCount : int16
2018-08-06 21:21:28 -05:00
}
2018-08-18 19:32:48 -05:00
/// Reset the "showAfter" property on a request
[<CLIMutable>]
type Show =
{ /// The time after which the request should appear
showAfter : int64
}
2018-08-06 21:21:28 -05: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
}
/// /api/journal URLs
module Journal =
/// GET /api/journal
let journal : HttpHandler =
authorize
>=> fun next ctx ->
2019-07-14 20:47:31 -05:00
task {
use sess = session ctx
let! jrnl = ((userId >> sess.JournalByUserId) ctx).ToListAsync ()
return! json jrnl next ctx
}
2018-08-06 21:21:28 -05:00
/// /api/request URLs
module Request =
open NCuid
/// POST /api/request
let add : HttpHandler =
authorize
>=> fun next ctx ->
task {
let! r = ctx.BindJsonAsync<Models.Request> ()
2019-07-13 22:55:53 -05:00
use sess = session ctx
2019-07-14 20:47:31 -05:00
let reqId = (Cuid.Generate >> toReqId) ()
2018-08-06 21:21:28 -05:00
let usrId = userId ctx
2019-07-14 20:47:31 -05:00
let now = jsNow ()
2019-07-13 22:55:53 -05:00
do! sess.AddRequest
{ Request.empty with
2019-07-27 20:02:01 -05:00
Id = RequestId.toString reqId
2019-07-13 22:55:53 -05:00
userId = usrId
enteredOn = now
showAfter = now
recurType = Recurrence.fromString r.recurType
recurCount = r.recurCount
history = [
{ History.empty with
asOf = now
status = "Created"
text = Some r.requestText
}
]
}
do! sess.SaveChangesAsync ()
match! sess.TryJournalById reqId usrId with
2018-08-14 21:01:21 -05:00
| Some req -> return! (setStatusCode 201 >=> json req) next ctx
2018-08-06 21:21:28 -05:00
| None -> return! Error.notFound next ctx
}
/// POST /api/request/[req-id]/history
2019-07-14 20:47:31 -05:00
let addHistory requestId : HttpHandler =
2018-08-06 21:21:28 -05:00
authorize
>=> fun next ctx ->
task {
2019-07-13 22:55:53 -05:00
use sess = session ctx
2019-07-14 20:47:31 -05:00
let reqId = toReqId requestId
2019-07-13 22:55:53 -05:00
match! sess.TryRequestById reqId (userId ctx) with
2018-08-14 21:01:21 -05:00
| Some req ->
2018-08-06 21:21:28 -05:00
let! hist = ctx.BindJsonAsync<Models.HistoryEntry> ()
2019-07-14 20:47:31 -05:00
let now = jsNow ()
2018-08-06 21:21:28 -05:00
{ History.empty with
2019-07-13 22:55:53 -05:00
asOf = now
status = hist.status
text = match hist.updateText with null | "" -> None | x -> Some x
2018-08-06 21:21:28 -05:00
}
2019-07-13 22:55:53 -05:00
|> sess.AddHistory reqId
2018-08-14 21:01:21 -05:00
match hist.status with
| "Prayed" ->
2019-07-27 20:02:01 -05:00
(Ticks.toLong now) + (Recurrence.duration req.recurType * int64 req.recurCount)
|> (Ticks >> sess.UpdateShowAfter reqId)
2018-08-14 21:01:21 -05:00
| _ -> ()
2019-07-13 22:55:53 -05:00
do! sess.SaveChangesAsync ()
2018-08-06 21:21:28 -05:00
return! created next ctx
| None -> return! Error.notFound next ctx
}
/// POST /api/request/[req-id]/note
2019-07-14 20:47:31 -05:00
let addNote requestId : HttpHandler =
2018-08-06 21:21:28 -05:00
authorize
>=> fun next ctx ->
task {
2019-07-14 20:47:31 -05:00
use sess = session ctx
let reqId = toReqId requestId
match! sess.TryRequestById reqId (userId ctx) with
2018-08-06 21:21:28 -05:00
| Some _ ->
let! notes = ctx.BindJsonAsync<Models.NoteEntry> ()
2019-07-14 20:47:31 -05:00
sess.AddNote reqId { asOf = jsNow (); notes = notes.notes }
do! sess.SaveChangesAsync ()
2018-08-06 21:21:28 -05:00
return! created next ctx
| None -> return! Error.notFound next ctx
}
/// GET /api/requests/answered
let answered : HttpHandler =
authorize
>=> fun next ctx ->
2019-07-14 20:47:31 -05:00
task {
use sess = session ctx
let! reqs = ((userId >> sess.AnsweredRequests) ctx).ToListAsync ()
return! json reqs next ctx
}
2018-08-06 21:21:28 -05:00
/// GET /api/request/[req-id]
2019-07-14 20:47:31 -05:00
let get requestId : HttpHandler =
2018-08-06 21:21:28 -05:00
authorize
>=> fun next ctx ->
task {
2019-07-13 22:55:53 -05:00
use sess = session ctx
2019-07-14 20:47:31 -05:00
match! sess.TryJournalById (toReqId requestId) (userId ctx) with
2018-08-14 21:01:21 -05:00
| Some req -> return! json req next ctx
2018-08-06 21:21:28 -05:00
| None -> return! Error.notFound next ctx
}
/// GET /api/request/[req-id]/full
2019-07-14 20:47:31 -05:00
let getFull requestId : HttpHandler =
2018-08-06 21:21:28 -05:00
authorize
>=> fun next ctx ->
task {
2019-07-13 22:55:53 -05:00
use sess = session ctx
2019-07-14 20:47:31 -05:00
match! sess.TryFullRequestById (toReqId requestId) (userId ctx) with
2018-08-14 21:01:21 -05:00
| Some req -> return! json req next ctx
2018-08-06 21:21:28 -05:00
| None -> return! Error.notFound next ctx
}
/// GET /api/request/[req-id]/notes
2019-07-14 20:47:31 -05:00
let getNotes requestId : HttpHandler =
2018-08-06 21:21:28 -05:00
authorize
>=> fun next ctx ->
task {
2019-07-14 20:47:31 -05:00
use sess = session ctx
let! notes = sess.NotesById (toReqId requestId) (userId ctx)
2018-08-06 21:21:28 -05:00
return! json notes next ctx
}
2018-08-18 19:32:48 -05:00
/// PATCH /api/request/[req-id]/show
2019-07-14 20:47:31 -05:00
let show requestId : HttpHandler =
2018-08-18 19:32:48 -05:00
authorize
>=> fun next ctx ->
task {
2019-07-14 20:47:31 -05:00
use sess = session ctx
let reqId = toReqId requestId
match! sess.TryRequestById reqId (userId ctx) with
| Some _ ->
2018-08-18 19:32:48 -05:00
let! show = ctx.BindJsonAsync<Models.Show> ()
2019-07-14 20:47:31 -05:00
sess.UpdateShowAfter reqId (Ticks show.showAfter)
do! sess.SaveChangesAsync ()
2018-08-18 19:32:48 -05:00
return! setStatusCode 204 next ctx
| None -> return! Error.notFound next ctx
}
/// PATCH /api/request/[req-id]/snooze
2019-07-14 20:47:31 -05:00
let snooze requestId : HttpHandler =
2018-08-06 21:21:28 -05:00
authorize
>=> fun next ctx ->
task {
2019-07-14 20:47:31 -05:00
use sess = session ctx
let reqId = toReqId requestId
match! sess.TryRequestById reqId (userId ctx) with
| Some _ ->
2018-08-06 21:21:28 -05:00
let! until = ctx.BindJsonAsync<Models.SnoozeUntil> ()
2019-07-14 20:47:31 -05:00
sess.UpdateSnoozed reqId (Ticks until.until)
do! sess.SaveChangesAsync ()
2018-08-06 21:21:28 -05:00
return! setStatusCode 204 next ctx
| None -> return! Error.notFound next ctx
}
2018-08-18 19:32:48 -05:00
/// PATCH /api/request/[req-id]/recurrence
2019-07-14 20:47:31 -05:00
let updateRecurrence requestId : HttpHandler =
2018-08-18 19:32:48 -05:00
authorize
>=> fun next ctx ->
task {
2019-07-14 20:47:31 -05:00
use sess = session ctx
let reqId = toReqId requestId
match! sess.TryRequestById reqId (userId ctx) with
| Some _ ->
2018-08-18 19:32:48 -05:00
let! recur = ctx.BindJsonAsync<Models.Recurrence> ()
2019-07-14 20:47:31 -05:00
sess.UpdateRecurrence reqId (Recurrence.fromString recur.recurType) recur.recurCount
do! sess.SaveChangesAsync ()
2018-08-18 19:32:48 -05:00
return! setStatusCode 204 next ctx
| None -> return! Error.notFound next ctx
}