Auth now works
Removed user match from all handlers; also updated API URLs in app
This commit is contained in:
parent
e4d00b4157
commit
07226f8cd8
|
@ -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> =
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user