F# API #18

Merged
danieljsummers merged 7 commits from fs-api into master 2018-08-07 02:21:29 +00:00
5 changed files with 143 additions and 148 deletions
Showing only changes of commit 07226f8cd8 - Show all commits

View File

@ -58,11 +58,14 @@ module Entities =
m.Property(fun e -> e.requestId).IsRequired () |> ignore
m.Property(fun e -> e.asOf).IsRequired () |> ignore
m.Property(fun e -> e.status).IsRequired() |> ignore
m.Property(fun e -> e.text) |> ignore
m.HasOne(fun e -> e.request)
.WithMany(fun r -> r.history :> IEnumerable<History>)
.HasForeignKey(fun e -> e.requestId :> obj)
|> ignore)
|> ignore
let typ = mb.Model.FindEntityType(typeof<History>)
let prop = typ.FindProperty("text")
mb.Model.FindEntityType(typeof<History>).FindProperty("text").SetValueConverter (OptionConverter<string> ())
/// Note is a note regarding a prayer request that does not result in an update to its text
@ -171,13 +174,9 @@ open System.Linq
open System.Threading.Tasks
/// Data context
type AppDbContext (opts : DbContextOptions<AppDbContext>) as self =
type AppDbContext (opts : DbContextOptions<AppDbContext>) =
inherit DbContext (opts)
/// Register a disconnected entity with the context, having the given state
let registerAs state (e : 'TEntity when 'TEntity : not struct) =
self.Entry<'TEntity>(e).State <- state
[<DefaultValue>]
val mutable private history : DbSet<History>
[<DefaultValue>]
@ -209,13 +208,17 @@ type AppDbContext (opts : DbContextOptions<AppDbContext>) as self =
]
|> List.iter (fun x -> x mb)
/// Register a disconnected entity with the context, having the given state
member private this.RegisterAs<'TEntity when 'TEntity : not struct> state e =
this.Entry<'TEntity>(e).State <- state
/// Add an entity instance to the context
member __.AddEntry e =
registerAs EntityState.Added e
member this.AddEntry e =
this.RegisterAs EntityState.Added e
/// Update the entity instance's values
member __.UpdateEntry e =
registerAs EntityState.Modified e
member this.UpdateEntry e =
this.RegisterAs EntityState.Modified e
/// Retrieve all answered requests for the given user
member this.AnsweredRequests userId : JournalRequest seq =
@ -227,7 +230,7 @@ type AppDbContext (opts : DbContextOptions<AppDbContext>) as self =
member this.JournalByUserId userId : JournalRequest seq =
upcast this.Journal
.Where(fun r -> r.userId = userId && r.lastStatus <> "Answered")
.OrderBy(fun r -> r.asOf)
.OrderByDescending(fun r -> r.asOf)
/// Retrieve a request by its ID and user ID
member this.TryRequestById reqId userId : Task<Request option> =

View File

@ -32,8 +32,8 @@ module Error =
module private Helpers =
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Authorization
open System.Threading.Tasks
open System.Security.Claims
/// Get the database context from DI
let db (ctx : HttpContext) =
@ -41,7 +41,12 @@ module private Helpers =
/// Get the user's "sub" claim
let user (ctx : HttpContext) =
ctx.User.Claims |> Seq.tryFind (fun u -> u.Type = "sub")
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 =
((user >> Option.get) ctx).Value
/// Return a 201 CREATED response
let created next ctx =
@ -51,20 +56,17 @@ module private Helpers =
let jsNow () =
DateTime.Now.Subtract(DateTime (1970, 1, 1)).TotalSeconds |> int64 |> (*) 1000L
/// 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 ->
task {
let auth = ctx.GetService<IAuthorizationService>()
let! result = auth.AuthorizeAsync (ctx.User, "LoggedOn")
Console.WriteLine (sprintf "*** Auth succeeded = %b" result.Succeeded)
match result.Succeeded with
| true -> return! next ctx
| false -> return! notAuthorized next ctx
}
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
@ -107,9 +109,9 @@ module Journal =
let journal : HttpHandler =
authorize
>=> fun next ctx ->
match user ctx with
| Some u -> json ((db ctx).JournalByUserId u.Value) next ctx
| None -> Error.notFound next ctx
userId ctx
|> (db ctx).JournalByUserId
|> asJson next ctx
/// /api/request URLs
@ -119,155 +121,141 @@ module Request =
/// POST /api/request
let add : HttpHandler =
fun next ctx ->
authorize
>=> fun next ctx ->
task {
match user ctx with
| Some u ->
let! r = ctx.BindJsonAsync<Models.Request> ()
let db = db ctx
let reqId = Cuid.Generate ()
let now = jsNow ()
{ Request.empty with
requestId = reqId
userId = u.Value
enteredOn = now
snoozedUntil = 0L
}
|> db.AddEntry
{ History.empty with
requestId = reqId
asOf = now
status = "Created"
text = Some r.requestText
}
|> db.AddEntry
let! _ = db.SaveChangesAsync ()
let! req = db.TryJournalById reqId u.Value
match req with
| Some rqst -> return! (setStatusCode 201 >=> json rqst) next ctx
| None -> return! Error.notFound next ctx
let! r = ctx.BindJsonAsync<Models.Request> ()
let db = db ctx
let reqId = Cuid.Generate ()
let usrId = userId ctx
let now = jsNow ()
{ Request.empty with
requestId = reqId
userId = usrId
enteredOn = now
snoozedUntil = 0L
}
|> db.AddEntry
{ History.empty with
requestId = reqId
asOf = now
status = "Created"
text = Some r.requestText
}
|> db.AddEntry
let! _ = db.SaveChangesAsync ()
let! req = db.TryJournalById reqId usrId
match req with
| Some rqst -> return! (setStatusCode 201 >=> json rqst) next ctx
| None -> return! Error.notFound next ctx
}
/// POST /api/request/[req-id]/history
let addHistory reqId : HttpHandler =
fun next ctx ->
authorize
>=> fun next ctx ->
task {
match user ctx with
| Some u ->
let db = db ctx
let! req = db.TryRequestById reqId u.Value
match req with
| Some _ ->
let! hist = ctx.BindJsonAsync<Models.HistoryEntry> ()
{ History.empty with
requestId = reqId
asOf = jsNow ()
status = hist.status
text = match hist.updateText with null | "" -> None | x -> Some x
}
|> db.AddEntry
let! _ = db.SaveChangesAsync ()
return! created next ctx
| None -> return! Error.notFound next ctx
let db = db ctx
let! req = db.TryRequestById reqId (userId ctx)
match req with
| Some _ ->
let! hist = ctx.BindJsonAsync<Models.HistoryEntry> ()
{ History.empty with
requestId = reqId
asOf = jsNow ()
status = hist.status
text = match hist.updateText with null | "" -> None | x -> Some x
}
|> db.AddEntry
let! _ = db.SaveChangesAsync ()
return! created next ctx
| None -> return! Error.notFound next ctx
}
/// POST /api/request/[req-id]/note
let addNote reqId : HttpHandler =
fun next ctx ->
authorize
>=> fun next ctx ->
task {
match user ctx with
| Some u ->
let db = db ctx
let! req = db.TryRequestById reqId u.Value
match req with
| Some _ ->
let! notes = ctx.BindJsonAsync<Models.NoteEntry> ()
{ Note.empty with
requestId = reqId
asOf = jsNow ()
notes = notes.notes
}
|> db.AddEntry
let! _ = db.SaveChangesAsync ()
return! created next ctx
| None -> return! Error.notFound next ctx
let db = db ctx
let! req = db.TryRequestById reqId (userId ctx)
match req with
| Some _ ->
let! notes = ctx.BindJsonAsync<Models.NoteEntry> ()
{ Note.empty with
requestId = reqId
asOf = jsNow ()
notes = notes.notes
}
|> db.AddEntry
let! _ = db.SaveChangesAsync ()
return! created next ctx
| None -> return! Error.notFound next ctx
}
/// GET /api/requests/answered
let answered : HttpHandler =
fun next ctx ->
match user ctx with
| Some u -> json ((db ctx).AnsweredRequests u.Value) next ctx
| None -> Error.notFound next ctx
authorize
>=> fun next ctx ->
userId ctx
|> (db ctx).AnsweredRequests
|> asJson next ctx
/// GET /api/request/[req-id]
let get reqId : HttpHandler =
fun next ctx ->
authorize
>=> fun next ctx ->
task {
match user ctx with
| Some u ->
let! req = (db ctx).TryRequestById reqId u.Value
match req with
| Some r -> return! json r next ctx
| None -> return! Error.notFound next ctx
let! req = (db ctx).TryRequestById reqId (userId ctx)
match req with
| Some r -> return! json r next ctx
| None -> return! Error.notFound next ctx
}
/// GET /api/request/[req-id]/complete
let getComplete reqId : HttpHandler =
fun next ctx ->
authorize
>=> fun next ctx ->
task {
match user ctx with
| Some u ->
let! req = (db ctx).TryCompleteRequestById reqId u.Value
match req with
| Some r -> return! json r next ctx
| None -> return! Error.notFound next ctx
let! req = (db ctx).TryCompleteRequestById reqId (userId ctx)
match req with
| Some r -> return! json r next ctx
| None -> return! Error.notFound next ctx
}
/// GET /api/request/[req-id]/full
let getFull reqId : HttpHandler =
fun next ctx ->
authorize
>=> fun next ctx ->
task {
match user ctx with
| Some u ->
let! req = (db ctx).TryFullRequestById reqId u.Value
match req with
| Some r -> return! json r next ctx
| None -> return! Error.notFound next ctx
let! req = (db ctx).TryFullRequestById reqId (userId ctx)
match req with
| Some r -> return! json r next ctx
| None -> return! Error.notFound next ctx
}
/// GET /api/request/[req-id]/notes
let getNotes reqId : HttpHandler =
fun next ctx ->
authorize
>=> fun next ctx ->
task {
match user ctx with
| Some u ->
let! notes = (db ctx).NotesById reqId u.Value
return! json notes next ctx
| None -> return! Error.notFound next ctx
let! notes = (db ctx).NotesById reqId (userId ctx)
return! json notes next ctx
}
/// POST /api/request/[req-id]/snooze
let snooze reqId : HttpHandler =
fun next ctx ->
authorize
>=> fun next ctx ->
task {
match user ctx with
| Some u ->
let db = db ctx
let! req = db.TryRequestById reqId u.Value
match req with
| Some r ->
let! until = ctx.BindJsonAsync<Models.SnoozeUntil> ()
{ r with snoozedUntil = until.until }
|> db.UpdateEntry
let! _ = db.SaveChangesAsync ()
return! setStatusCode 204 next ctx
| None -> return! Error.notFound next ctx
let db = db ctx
let! req = db.TryRequestById reqId (userId ctx)
match req with
| Some r ->
let! until = ctx.BindJsonAsync<Models.SnoozeUntil> ()
{ r with snoozedUntil = until.until }
|> db.UpdateEntry
let! _ = db.SaveChangesAsync ()
return! setStatusCode 204 next ctx
| None -> return! Error.notFound next ctx
}

View File

@ -16,6 +16,7 @@
<PackageReference Include="Giraffe.TokenRouter" Version="0.1.0-beta-110" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NCuid.NetCore" Version="1.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.1.1" />
</ItemGroup>
<ItemGroup>

View File

@ -2,18 +2,21 @@ namespace MyPrayerJournal.Api
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open System
/// Configuration functions for the application
module Configure =
open Giraffe
open Giraffe.TokenRouter
open Microsoft.AspNetCore.Authentication.JwtBearer
open Microsoft.AspNetCore.Server.Kestrel.Core
open Microsoft.EntityFrameworkCore
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Logging
open Giraffe
open Giraffe.TokenRouter
open MyPrayerJournal
/// Set up the configuration for the app
let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) =
@ -29,21 +32,22 @@ module Configure =
/// Configure dependency injection
let services (sc : IServiceCollection) =
sc.AddGiraffe () |> ignore
// mad props to Andrea Chiarelli @ https://auth0.com/blog/securing-asp-dot-net-core-2-applications-with-jwts/
use sp = sc.BuildServiceProvider()
let cfg = sp.GetRequiredService<IConfiguration>().GetSection "Auth0"
sc.AddAuthentication(
fun opts ->
opts.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme
opts.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer (
let cfg = sp.GetRequiredService<IConfiguration> ()
sc.AddGiraffe()
.AddAuthentication(
/// Use HTTP "Bearer" authentication with JWTs
fun opts ->
opts.Authority <- sprintf "https://%s/" cfg.["Domain"]
opts.Audience <- cfg.["Audience"]
opts.TokenValidationParameters.ValidateAudience <- false)
opts.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme
opts.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
/// Configure JWT options with Auth0 options from configuration
fun opts ->
let jwtCfg = cfg.GetSection "Auth0"
opts.Authority <- sprintf "https://%s/" jwtCfg.["Domain"]
opts.Audience <- jwtCfg.["Id"])
|> ignore
sc.AddAuthorization (fun opts -> opts.AddPolicy ("LoggedOn", fun p -> p.RequireClaim "sub" |> ignore))
sc.AddDbContext<AppDbContext>(fun opts -> opts.UseNpgsql(cfg.GetConnectionString "mpj") |> ignore)
|> ignore
/// Routes for the available URLs within myPrayerJournal
@ -96,12 +100,11 @@ module Configure =
module Program =
open System
open System.IO
let exitCode = 0
let CreateWebHostBuilder args =
let CreateWebHostBuilder _ =
let contentRoot = Directory.GetCurrentDirectory ()
WebHostBuilder()
.UseContentRoot(contentRoot)

View File

@ -31,12 +31,12 @@ export default {
* Add a new prayer request
* @param {string} requestText The text of the request to be added
*/
addRequest: requestText => http.post('request/', { requestText }),
addRequest: requestText => http.post('request', { requestText }),
/**
* Get all answered requests, along with the text they had when it was answered
*/
getAnsweredRequests: () => http.get('request/answered'),
getAnsweredRequests: () => http.get('requests/answered'),
/**
* Get a prayer request (full; includes all history)
@ -64,7 +64,7 @@ export default {
/**
* Get all prayer requests and their most recent updates
*/
journal: () => http.get('journal/'),
journal: () => http.get('journal'),
/**
* Update a prayer request