Moved Suave app to /api directory
This commit is contained in:
217
src/api/App.fs
Normal file
217
src/api/App.fs
Normal file
@@ -0,0 +1,217 @@
|
||||
/// Main server module for myPrayerJournal
|
||||
module MyPrayerJournal.App
|
||||
|
||||
open Auth0.AuthenticationApi
|
||||
open Auth0.AuthenticationApi.Models
|
||||
open Microsoft.EntityFrameworkCore
|
||||
open Newtonsoft.Json
|
||||
open Newtonsoft.Json.Linq
|
||||
open Reader
|
||||
open System
|
||||
open System.IO
|
||||
open Suave
|
||||
open Suave.Filters
|
||||
open Suave.Operators
|
||||
open Suave.Redirection
|
||||
open Suave.RequestErrors
|
||||
open Suave.State.CookieStateStore
|
||||
open Suave.Successful
|
||||
|
||||
let utf8 = System.Text.Encoding.UTF8
|
||||
|
||||
type JsonNetCookieSerializer () =
|
||||
interface CookieSerialiser with
|
||||
member x.serialise m =
|
||||
utf8.GetBytes (JsonConvert.SerializeObject m)
|
||||
member x.deserialise m =
|
||||
JsonConvert.DeserializeObject<Map<string, obj>> (utf8.GetString m)
|
||||
|
||||
type Auth0Config = {
|
||||
Domain : string
|
||||
ClientId : string
|
||||
ClientSecret : string
|
||||
}
|
||||
with
|
||||
static member empty =
|
||||
{ Domain = ""
|
||||
ClientId = ""
|
||||
ClientSecret = ""
|
||||
}
|
||||
|
||||
type Config = {
|
||||
Conn : string
|
||||
Auth0 : Auth0Config
|
||||
}
|
||||
with
|
||||
static member empty =
|
||||
{ Conn = ""
|
||||
Auth0 = Auth0Config.empty
|
||||
}
|
||||
|
||||
let cfg =
|
||||
try
|
||||
use sr = File.OpenText "appsettings.json"
|
||||
let settings = JToken.ReadFrom(new JsonTextReader(sr)) :?> JObject
|
||||
{ Conn = settings.["conn"].ToObject<string>()
|
||||
Auth0 =
|
||||
{ Domain = settings.["auth0"].["domain"].ToObject<string>()
|
||||
ClientId = settings.["auth0"].["client-id"].ToObject<string>()
|
||||
ClientSecret = settings.["auth0"].["client-secret"].ToObject<string>()
|
||||
}
|
||||
}
|
||||
with _ -> Config.empty
|
||||
|
||||
/// Data Configuration singleton
|
||||
//let lazyCfg = lazy (DataConfig.FromJson <| try File.ReadAllText "data-config.json" with _ -> "{}")
|
||||
/// RethinkDB connection singleton
|
||||
//let lazyConn = lazy lazyCfg.Force().CreateConnection ()
|
||||
/// Application dependencies
|
||||
//let deps = {
|
||||
// new IDependencies with
|
||||
// member __.Conn with get () = lazyConn.Force ()
|
||||
// }
|
||||
|
||||
/// Get the scheme, host, and port of the URL
|
||||
let schemeHostPort (req : HttpRequest) =
|
||||
sprintf "%s://%s" req.url.Scheme (req.headers |> List.filter (fun x -> fst x = "host") |> List.head |> snd)
|
||||
|
||||
/// Authorization functions
|
||||
module Auth =
|
||||
|
||||
open Views
|
||||
|
||||
let exchangeCodeForToken code = context (fun ctx ->
|
||||
async {
|
||||
let client = AuthenticationApiClient (Uri (sprintf "https://%s" cfg.Auth0.Domain))
|
||||
let! req =
|
||||
client.ExchangeCodeForAccessTokenAsync
|
||||
(ExchangeCodeRequest
|
||||
(AuthorizationCode = code,
|
||||
ClientId = cfg.Auth0.ClientId,
|
||||
ClientSecret = cfg.Auth0.ClientSecret,
|
||||
RedirectUri = sprintf "%s/user/log-on" (schemeHostPort ctx.request)))
|
||||
let! user = client.GetUserInfoAsync ((req : AccessToken).AccessToken)
|
||||
return
|
||||
ctx
|
||||
|> HttpContext.state
|
||||
|> function
|
||||
| None -> FORBIDDEN "Cannot sign in without state"
|
||||
| Some state ->
|
||||
state.set "auth-token" req.IdToken
|
||||
>=> Writers.setUserData "user" user
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
)
|
||||
|
||||
/// Handle the sign-in callback from Auth0
|
||||
let handleSignIn =
|
||||
context (fun ctx ->
|
||||
GET
|
||||
>=> match ctx.request.queryParam "code" with
|
||||
| Choice1Of2 authCode ->
|
||||
exchangeCodeForToken authCode
|
||||
>=> FOUND (sprintf "%s/journal" (schemeHostPort ctx.request))
|
||||
| Choice2Of2 msg -> BAD_REQUEST msg
|
||||
)
|
||||
|
||||
/// Handle signing out a user
|
||||
let handleSignOut =
|
||||
context (fun ctx ->
|
||||
match ctx |> HttpContext.state with
|
||||
| Some state -> state.set "auth-key" null
|
||||
| _ -> succeed
|
||||
>=> FOUND (sprintf "%s/" (schemeHostPort ctx.request)))
|
||||
|
||||
let cw (x : string) = Console.WriteLine x
|
||||
|
||||
/// Convert microtime to ticks, add difference from 1/1/1 to 1/1/1970
|
||||
let jsDate jsTicks =
|
||||
DateTime(jsTicks * 10000000L).AddTicks(DateTime(1970, 1, 1).Ticks)
|
||||
|
||||
let getIdFromToken token =
|
||||
match token with
|
||||
| Some jwt ->
|
||||
try
|
||||
let key = Convert.FromBase64String(cfg.Auth0.ClientSecret.Replace("-", "+").Replace("_", "/"))
|
||||
let payload = Jose.JWT.Decode<JObject>(jwt, key)
|
||||
let tokenExpires = jsDate (payload.["exp"].ToObject<int64>())
|
||||
match tokenExpires > DateTime.UtcNow with
|
||||
| true -> Some (payload.["sub"].ToObject<string>())
|
||||
| _ -> None
|
||||
with ex ->
|
||||
sprintf "Token Deserialization Exception - %s" (ex.GetType().FullName) |> cw
|
||||
sprintf "Message - %s" ex.Message |> cw
|
||||
ex.StackTrace |> cw
|
||||
None
|
||||
| _ -> None
|
||||
|
||||
/// Add the logged on user Id to the context if it exists
|
||||
let loggedOn = warbler (fun ctx ->
|
||||
match HttpContext.state ctx with
|
||||
| Some state -> Writers.setUserData "user" (state.get "auth-token" |> getIdFromToken)
|
||||
| _ -> Writers.setUserData "user" None)
|
||||
|
||||
/// Create a user context for the currently assigned user
|
||||
let userCtx ctx = { Id = ctx.userState.["user"] :?> string option }
|
||||
|
||||
/// Read an item from the user state, downcast to the expected type
|
||||
let read ctx key : 'value =
|
||||
ctx.userState |> Map.tryFind key |> Option.map (fun x -> x :?> 'value) |> Option.get
|
||||
|
||||
/// Create a new data context
|
||||
let dataCtx () =
|
||||
new DataContext (((DbContextOptionsBuilder<DataContext>()).UseNpgsql cfg.Conn).Options)
|
||||
|
||||
/// Return an HTML page
|
||||
let html ctx content =
|
||||
Views.page (Auth.userCtx ctx) content
|
||||
|
||||
/// Home page
|
||||
let viewHome = warbler (fun ctx -> OK (Views.home |> html ctx))
|
||||
|
||||
/// Journal page
|
||||
let viewJournal =
|
||||
context (fun ctx ->
|
||||
use dataCtx = dataCtx ()
|
||||
let reqs = Data.Requests.allForUser (defaultArg (read ctx "user") "") dataCtx
|
||||
OK (Views.journal reqs |> html ctx))
|
||||
|
||||
/// Suave application
|
||||
let app =
|
||||
statefulForSession
|
||||
>=> Auth.loggedOn
|
||||
>=> choose [
|
||||
path Route.home >=> viewHome
|
||||
path Route.journal >=> viewJournal
|
||||
path Route.User.logOn >=> Auth.handleSignIn
|
||||
path Route.User.logOff >=> Auth.handleSignOut
|
||||
Files.browseHome
|
||||
NOT_FOUND "Page not found."
|
||||
]
|
||||
|
||||
/// Ensure the EF context is created in the right format
|
||||
let ensureDatabase () =
|
||||
async {
|
||||
use data = dataCtx ()
|
||||
do! data.Database.MigrateAsync ()
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
let suaveCfg =
|
||||
{ defaultConfig with
|
||||
homeFolder = Some (Path.GetFullPath "./wwwroot/")
|
||||
serverKey = Text.Encoding.UTF8.GetBytes("12345678901234567890123456789012")
|
||||
cookieSerialiser = JsonNetCookieSerializer ()
|
||||
}
|
||||
|
||||
[<EntryPoint>]
|
||||
let main argv =
|
||||
// Establish the data environment
|
||||
//liftDep getConn (Data.establishEnvironment >> Async.RunSynchronously)
|
||||
//|> run deps
|
||||
ensureDatabase ()
|
||||
startWebServer suaveCfg app
|
||||
0
|
||||
(*
|
||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2Rqcy1jb25zdWx0aW5nLmF1dGgwLmNvbS8iLCJzdWIiOiJ3aW5kb3dzbGl2ZXw3OTMyNGZhMTM4MzZlZGNiIiwiYXVkIjoiT2YyczBSUUNRM210M2R3SWtPQlk1aDg1SjlzWGJGMm4iLCJleHAiOjE0OTI5MDc1OTAsImlhdCI6MTQ5Mjg3MTU5MH0.61JPm3Hz7XW-iaSq8Esv1cajQPbK0o9L5xz-RHIYq9g
|
||||
*)
|
||||
57
src/api/Data.fs
Normal file
57
src/api/Data.fs
Normal file
@@ -0,0 +1,57 @@
|
||||
namespace MyPrayerJournal
|
||||
|
||||
open Microsoft.EntityFrameworkCore
|
||||
open System.Linq
|
||||
open System.Runtime.CompilerServices
|
||||
|
||||
/// Data context for myPrayerJournal
|
||||
type DataContext =
|
||||
inherit DbContext
|
||||
|
||||
(*--- CONSTRUCTORS ---*)
|
||||
|
||||
new () = { inherit DbContext () }
|
||||
new (options : DbContextOptions<DataContext>) = { inherit DbContext (options) }
|
||||
|
||||
(*--- DbSet FIELDS ---*)
|
||||
|
||||
[<DefaultValue>]
|
||||
val mutable private requests : DbSet<Request>
|
||||
[<DefaultValue>]
|
||||
val mutable private history : DbSet<History>
|
||||
|
||||
(*--- DbSet PROPERTIES ---*)
|
||||
|
||||
/// Prayer Requests
|
||||
member this.Requests with get () = this.requests and set v = this.requests <- v
|
||||
|
||||
/// History
|
||||
member this.History with get () = this.history and set v = this.history <- v
|
||||
|
||||
override this.OnModelCreating (modelBuilder) =
|
||||
base.OnModelCreating modelBuilder
|
||||
|
||||
modelBuilder.HasDefaultSchema "mpj"
|
||||
|> Request.ConfigureEF
|
||||
|> History.ConfigureEF
|
||||
|> ignore
|
||||
|
||||
/// Data access
|
||||
module Data =
|
||||
|
||||
/// Data access for prayer requests
|
||||
module Requests =
|
||||
|
||||
/// Get all prayer requests for a user
|
||||
let allForUser userId (ctx : DataContext) =
|
||||
query {
|
||||
for req in ctx.Requests do
|
||||
where (req.UserId = userId)
|
||||
select req
|
||||
}
|
||||
|> Seq.sortBy
|
||||
(fun req ->
|
||||
match req.History |> Seq.sortBy (fun hist -> hist.AsOf) |> Seq.tryLast with
|
||||
| Some hist -> hist.AsOf
|
||||
| _ -> 0L)
|
||||
|> List.ofSeq
|
||||
48
src/api/Dependencies.fs
Normal file
48
src/api/Dependencies.fs
Normal file
@@ -0,0 +1,48 @@
|
||||
namespace MyPrayerJournal
|
||||
|
||||
//open RethinkDb.Driver.Net
|
||||
|
||||
// -- begin code lifted from #er demo --
|
||||
type ReaderM<'d, 'out> = 'd -> 'out
|
||||
|
||||
module Reader =
|
||||
// basic operations
|
||||
let run dep (rm : ReaderM<_,_>) = rm dep
|
||||
let constant (c : 'c) : ReaderM<_,'c> = fun _ -> c
|
||||
// lifting of functions and state
|
||||
let lift1 (f : 'd -> 'a -> 'out) : 'a -> ReaderM<'d, 'out> = fun a dep -> f dep a
|
||||
let lift2 (f : 'd -> 'a -> 'b -> 'out) : 'a -> 'b -> ReaderM<'d, 'out> = fun a b dep -> f dep a b
|
||||
let lift3 (f : 'd -> 'a -> 'b -> 'c -> 'out) : 'a -> 'b -> 'c -> ReaderM<'d, 'out> = fun a b c dep -> f dep a b c
|
||||
let liftDep (proj : 'd2 -> 'd1) (rm : ReaderM<'d1, 'output>) : ReaderM<'d2, 'output> = proj >> rm
|
||||
// functor
|
||||
let fmap (f : 'a -> 'b) (g : 'c -> 'a) : ('c -> 'b) = g >> f
|
||||
let map (f : 'a -> 'b) (rm : ReaderM<'d, 'a>) : ReaderM<'d,'b> = rm >> f
|
||||
let (<?>) = map
|
||||
// applicative-functor
|
||||
let apply (f : ReaderM<'d, 'a->'b>) (rm : ReaderM<'d, 'a>) : ReaderM<'d, 'b> =
|
||||
fun dep ->
|
||||
let f' = run dep f
|
||||
let a = run dep rm
|
||||
f' a
|
||||
let (<*>) = apply
|
||||
// monad
|
||||
let bind (rm : ReaderM<'d, 'a>) (f : 'a -> ReaderM<'d,'b>) : ReaderM<'d, 'b> =
|
||||
fun dep ->
|
||||
f (rm dep)
|
||||
|> run dep
|
||||
let (>>=) = bind
|
||||
type ReaderMBuilder internal () =
|
||||
member __.Bind(m, f) = m >>= f
|
||||
member __.Return(v) = constant v
|
||||
member __.ReturnFrom(v) = v
|
||||
member __.Delay(f) = f ()
|
||||
let reader = ReaderMBuilder()
|
||||
// -- end code lifted from #er demo --
|
||||
|
||||
(*type IDependencies =
|
||||
abstract Conn : IConnection
|
||||
|
||||
[<AutoOpen>]
|
||||
module DependencyExtraction =
|
||||
|
||||
let getConn (deps : IDependencies) = deps.Conn*)
|
||||
131
src/api/Entities.fs
Normal file
131
src/api/Entities.fs
Normal file
@@ -0,0 +1,131 @@
|
||||
namespace MyPrayerJournal
|
||||
|
||||
open Microsoft.EntityFrameworkCore;
|
||||
open Newtonsoft.Json
|
||||
open System
|
||||
open System.Collections.Generic
|
||||
|
||||
/// A prayer request
|
||||
[<AllowNullLiteral>]
|
||||
type Request() =
|
||||
/// The history collection (can be overridden)
|
||||
let mutable historyCollection : ICollection<History> = upcast List<History> ()
|
||||
|
||||
/// The Id of the prayer request
|
||||
member val RequestId = Guid.Empty with get, set
|
||||
/// The Id of the user to whom the request belongs
|
||||
member val UserId = "" with get, set
|
||||
/// The ticks when the request was entered
|
||||
member val EnteredOn = 0L with get, set
|
||||
|
||||
/// The history for the prayer request
|
||||
abstract History : ICollection<History> with get, set
|
||||
default this.History
|
||||
with get () = historyCollection
|
||||
and set v = historyCollection <- v
|
||||
|
||||
static member ConfigureEF (mb : ModelBuilder) =
|
||||
mb.Entity<Request>().ToTable "Request"
|
||||
|> ignore
|
||||
mb
|
||||
|
||||
|
||||
/// A historial update to a prayer request
|
||||
and [<AllowNullLiteral>] History() =
|
||||
/// The request to which this entry applies (may be overridden)
|
||||
let mutable request = null
|
||||
|
||||
/// The Id of the request to which this update applies
|
||||
member val RequestId = Guid.Empty with get, set
|
||||
/// The ticks when this entry was made
|
||||
member val AsOf = 0L with get, set
|
||||
/// The status of the request as of this history entry
|
||||
member val Status = "" with get, set
|
||||
/// The text of this history entry
|
||||
member val Text = "" with get, set
|
||||
|
||||
/// The request to which this entry belongs
|
||||
abstract Request : Request with get, set
|
||||
default this.Request
|
||||
with get () = request
|
||||
and set v = request <- v
|
||||
|
||||
static member ConfigureEF (mb : ModelBuilder) =
|
||||
mb.Entity<History>().ToTable("History")
|
||||
|> ignore
|
||||
mb.Entity<History>().HasKey(fun e -> (e.RequestId, e.AsOf) :> obj)
|
||||
|> ignore
|
||||
mb
|
||||
|
||||
(*
|
||||
/// A user
|
||||
type Userr = {
|
||||
/// The Id of the user
|
||||
[<JsonProperty("id")>]
|
||||
Id : string
|
||||
/// The user's e-mail address
|
||||
Email : string
|
||||
/// The user's name
|
||||
Name : string
|
||||
/// The time zone in which the user resides
|
||||
TimeZone : string
|
||||
/// The last time the user logged on
|
||||
LastSeenOn : int64
|
||||
}
|
||||
with
|
||||
/// An empty User
|
||||
static member Empty =
|
||||
{ Id = ""
|
||||
Email = ""
|
||||
Name = ""
|
||||
TimeZone = ""
|
||||
LastSeenOn = int64 0 }
|
||||
|
||||
|
||||
/// Request history entry
|
||||
type Historyy = {
|
||||
/// The instant at which the update was made
|
||||
AsOf : int64
|
||||
/// The action that was taken on the request
|
||||
Action : string list
|
||||
/// The status of the request (filled if it changed)
|
||||
Status : string option
|
||||
/// The text of the request (filled if it changed)
|
||||
Text : string option
|
||||
}
|
||||
|
||||
/// A prayer request
|
||||
type Requestt = {
|
||||
/// The Id of the request
|
||||
[<JsonProperty("id")>]
|
||||
Id : string
|
||||
/// The Id of the user to whom this request belongs
|
||||
UserId : string
|
||||
/// The instant this request was entered
|
||||
EnteredOn : int64
|
||||
/// The history for this request
|
||||
History : Historyy list
|
||||
}
|
||||
with
|
||||
/// The current status of the prayer request
|
||||
member this.Status =
|
||||
this.History
|
||||
|> List.sortBy (fun item -> -item.AsOf)
|
||||
|> List.map (fun item -> item.Status)
|
||||
|> List.filter Option.isSome
|
||||
|> List.map Option.get
|
||||
|> List.head
|
||||
/// The current text of the prayer request
|
||||
member this.Text =
|
||||
this.History
|
||||
|> List.sortBy (fun item -> -item.AsOf)
|
||||
|> List.map (fun item -> item.Text)
|
||||
|> List.filter Option.isSome
|
||||
|> List.map Option.get
|
||||
|> List.head
|
||||
member this.LastActionOn =
|
||||
this.History
|
||||
|> List.sortBy (fun item -> -item.AsOf)
|
||||
|> List.map (fun item -> item.AsOf)
|
||||
|> List.head
|
||||
*)
|
||||
16
src/api/Extensions.fs
Normal file
16
src/api/Extensions.fs
Normal file
@@ -0,0 +1,16 @@
|
||||
[<AutoOpen>]
|
||||
module MyPrayerJournal.Extensions
|
||||
|
||||
open System.Threading.Tasks
|
||||
|
||||
// H/T: Suave
|
||||
type AsyncBuilder with
|
||||
/// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
|
||||
/// a standard .NET task
|
||||
member x.Bind(t : Task<'T>, f:'T -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f)
|
||||
|
||||
/// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
|
||||
/// a standard .NET task which does not commpute a value
|
||||
member x.Bind(t : Task, f : unit -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f)
|
||||
|
||||
member x.ReturnFrom(t : Task<'T>) : Async<'T> = Async.AwaitTask t
|
||||
87
src/api/Migrations/20170104023341_InitialDb.fs
Normal file
87
src/api/Migrations/20170104023341_InitialDb.fs
Normal file
@@ -0,0 +1,87 @@
|
||||
namespace MyPrayerJournal.Migrations
|
||||
|
||||
open System
|
||||
open System.Collections.Generic
|
||||
open Microsoft.EntityFrameworkCore
|
||||
open Microsoft.EntityFrameworkCore.Infrastructure
|
||||
open Microsoft.EntityFrameworkCore.Metadata
|
||||
open Microsoft.EntityFrameworkCore.Migrations
|
||||
open Microsoft.EntityFrameworkCore.Migrations.Operations
|
||||
open Microsoft.EntityFrameworkCore.Migrations.Operations.Builders
|
||||
open MyPrayerJournal
|
||||
|
||||
type RequestTable = {
|
||||
RequestId : OperationBuilder<AddColumnOperation>
|
||||
EnteredOn : OperationBuilder<AddColumnOperation>
|
||||
UserId : OperationBuilder<AddColumnOperation>
|
||||
}
|
||||
|
||||
type HistoryTable = {
|
||||
RequestId : OperationBuilder<AddColumnOperation>
|
||||
AsOf : OperationBuilder<AddColumnOperation>
|
||||
Status : OperationBuilder<AddColumnOperation>
|
||||
Text : OperationBuilder<AddColumnOperation>
|
||||
}
|
||||
|
||||
[<DbContext (typeof<DataContext>)>]
|
||||
[<Migration "20170104023341_InitialDb">]
|
||||
type InitialDb () =
|
||||
inherit Migration ()
|
||||
|
||||
override this.Up migrationBuilder =
|
||||
migrationBuilder.EnsureSchema(
|
||||
name = "mpj")
|
||||
|> ignore
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name = "Request",
|
||||
schema = "mpj",
|
||||
columns =
|
||||
(fun table ->
|
||||
{ RequestId = table.Column<Guid>(nullable = false)
|
||||
EnteredOn = table.Column<int64>(nullable = false)
|
||||
UserId = table.Column<string>(nullable = false)
|
||||
}
|
||||
),
|
||||
constraints =
|
||||
fun table ->
|
||||
table.PrimaryKey("PK_Request", fun x -> x.RequestId :> obj) |> ignore
|
||||
)
|
||||
|> ignore
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name = "History",
|
||||
schema = "mpj",
|
||||
columns =
|
||||
(fun table ->
|
||||
{ RequestId = table.Column<Guid>(nullable = false)
|
||||
AsOf = table.Column<int64>(nullable = false)
|
||||
Status = table.Column<string>(nullable = true)
|
||||
Text = table.Column<string>(nullable = true)
|
||||
}
|
||||
),
|
||||
constraints =
|
||||
fun table ->
|
||||
table.PrimaryKey("PK_History", fun x -> (x.RequestId, x.AsOf) :> obj)
|
||||
|> ignore
|
||||
table.ForeignKey(
|
||||
name = "FK_History_Request_RequestId",
|
||||
column = (fun x -> x.RequestId :> obj),
|
||||
principalSchema = "mpj",
|
||||
principalTable = "Request",
|
||||
principalColumn = "RequestId",
|
||||
onDelete = ReferentialAction.Cascade)
|
||||
|> ignore
|
||||
)
|
||||
|> ignore
|
||||
|
||||
override this.Down migrationBuilder =
|
||||
migrationBuilder.DropTable(
|
||||
name = "History",
|
||||
schema = "mpj")
|
||||
|> ignore
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name = "Request",
|
||||
schema = "mpj")
|
||||
|> ignore
|
||||
61
src/api/Migrations/DataContextModelSnapshot.fs
Normal file
61
src/api/Migrations/DataContextModelSnapshot.fs
Normal file
@@ -0,0 +1,61 @@
|
||||
namespace MyPrayerJournal.Migrations
|
||||
|
||||
open System
|
||||
open Microsoft.EntityFrameworkCore
|
||||
open Microsoft.EntityFrameworkCore.Infrastructure
|
||||
open Microsoft.EntityFrameworkCore.Metadata
|
||||
open Microsoft.EntityFrameworkCore.Migrations
|
||||
open MyPrayerJournal
|
||||
|
||||
[<DbContext (typeof<DataContext>)>]
|
||||
type DataContextModelSnapshot () =
|
||||
inherit ModelSnapshot ()
|
||||
override this.BuildModel modelBuilder =
|
||||
modelBuilder
|
||||
.HasDefaultSchema("mpj")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn)
|
||||
.HasAnnotation("ProductVersion", "1.1.0-rtm-22752")
|
||||
|> ignore
|
||||
|
||||
modelBuilder.Entity("MyPrayerJournal.History",
|
||||
fun b ->
|
||||
b.Property<Guid>("RequestId")
|
||||
|> ignore
|
||||
b.Property<int64>("AsOf")
|
||||
|> ignore
|
||||
b.Property<string>("Status")
|
||||
|> ignore
|
||||
b.Property<string>("Text")
|
||||
|> ignore
|
||||
b.HasKey("RequestId", "AsOf")
|
||||
|> ignore
|
||||
b.ToTable("History")
|
||||
|> ignore
|
||||
)
|
||||
|> ignore
|
||||
|
||||
modelBuilder.Entity("MyPrayerJournal.Request",
|
||||
fun b ->
|
||||
b.Property<Guid>("RequestId")
|
||||
.ValueGeneratedOnAdd()
|
||||
|> ignore
|
||||
b.Property<int64>("EnteredOn")
|
||||
|> ignore
|
||||
b.Property<string>("UserId")
|
||||
|> ignore
|
||||
b.HasKey("RequestId")
|
||||
|> ignore
|
||||
b.ToTable("Request")
|
||||
|> ignore
|
||||
)
|
||||
|> ignore
|
||||
|
||||
modelBuilder.Entity("MyPrayerJournal.History",
|
||||
fun b ->
|
||||
b.HasOne("MyPrayerJournal.Request", "Request")
|
||||
.WithMany("History")
|
||||
.HasForeignKey("RequestId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
|> ignore
|
||||
)
|
||||
|> ignore
|
||||
51
src/api/MyPrayerJournal.fsproj
Normal file
51
src/api/MyPrayerJournal.fsproj
Normal file
@@ -0,0 +1,51 @@
|
||||
<Project Sdk="FSharp.NET.Sdk;Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>0.8.1</VersionPrefix>
|
||||
<TargetFramework>netcoreapp1.1</TargetFramework>
|
||||
<DebugType>portable</DebugType>
|
||||
<AssemblyName>myPrayerJournal</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<PackageId>src</PackageId>
|
||||
<PackageTargetFallback>$(PackageTargetFallback);dnxcore50</PackageTargetFallback>
|
||||
<RuntimeFrameworkVersion>1.1.1</RuntimeFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Extensions.fs" />
|
||||
<Compile Include="Entities.fs" />
|
||||
<Compile Include="Dependencies.fs" />
|
||||
<Compile Include="Data.fs" />
|
||||
<Compile Include="Migrations/20170104023341_InitialDb.fs" />
|
||||
<Compile Include="Migrations/DataContextModelSnapshot.fs" />
|
||||
<Compile Include="Route.fs" />
|
||||
<Compile Include="Views.fs" />
|
||||
<Compile Include="App.fs" />
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FSharp.NET.Sdk" Version="1.0.*" PrivateAssets="All" />
|
||||
<PackageReference Include="FSharp.Core" Version="4.1.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Auth0.AuthenticationApi" Version="3.6.0" />
|
||||
<PackageReference Include="jose-jwt" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.0.0">
|
||||
<PrivateAssets>All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="1.1.0" />
|
||||
<PackageReference Include="Suave" Version="2.0.0" />
|
||||
<PackageReference Include="Suave.Experimental" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="dotnet-compile-fsc" Version="1.0.0-preview2.1-*" />
|
||||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
15
src/api/Route.fs
Normal file
15
src/api/Route.fs
Normal file
@@ -0,0 +1,15 @@
|
||||
/// URL routes for myPrayerJournal
|
||||
module MyPrayerJournal.Route
|
||||
|
||||
/// The home page
|
||||
let home = "/"
|
||||
|
||||
/// The main journal page
|
||||
let journal = "/journal"
|
||||
|
||||
/// Routes dealing with users
|
||||
module User =
|
||||
/// The route for user log on response from Auth0
|
||||
let logOn = "/user/log-on"
|
||||
let logOff = "/user/log-off"
|
||||
|
||||
113
src/api/Views.fs
Normal file
113
src/api/Views.fs
Normal file
@@ -0,0 +1,113 @@
|
||||
module MyPrayerJournal.Views
|
||||
|
||||
//open Suave.Html
|
||||
open Suave.Xml
|
||||
|
||||
type UserContext = { Id: string option }
|
||||
|
||||
[<AutoOpen>]
|
||||
module Tags =
|
||||
/// Generate a meta tag
|
||||
let meta attr = tag "meta" attr empty
|
||||
|
||||
/// Generate a link to a stylesheet
|
||||
let stylesheet url = linkAttr [ "rel", "stylesheet"; "href", url ]
|
||||
|
||||
let aAttr attr x = tag "a" attr (flatten x)
|
||||
let a = aAttr []
|
||||
let buttonAttr attr x = tag "button" attr (flatten x)
|
||||
let button = buttonAttr []
|
||||
|
||||
let footerAttr attr x = tag "footer" attr (flatten x)
|
||||
let footer = footerAttr []
|
||||
let ulAttr attr x = tag "ul" attr (flatten x)
|
||||
let ul = ulAttr []
|
||||
|
||||
/// Used to prevent a self-closing tag where we need no text
|
||||
let noText = text ""
|
||||
let navLinkAttr attr url linkText = aAttr (("href", url) :: attr) [ text linkText ]
|
||||
|
||||
let navLink = navLinkAttr []
|
||||
|
||||
let jsLink func linkText = navLinkAttr [ "onclick", func ] "javascript:void(0)" linkText
|
||||
|
||||
/// Create a link to a JavaScript file
|
||||
let js src = scriptAttr [ "src", src ] [ noText ]
|
||||
|
||||
[<AutoOpen>]
|
||||
module PageComponents =
|
||||
let prependDoctype document = sprintf "<!DOCTYPE html>\n%s" document
|
||||
let render = xmlToString >> prependDoctype
|
||||
|
||||
let navigation userCtx =
|
||||
[
|
||||
match userCtx.Id with
|
||||
| Some _ ->
|
||||
yield navLink Route.journal "Journal"
|
||||
yield navLink Route.User.logOff "Log Off"
|
||||
| _ -> yield jsLink "mpj.signIn()" "Log On"
|
||||
|
||||
]
|
||||
|> List.map (fun x -> tag "li" [] x)
|
||||
let pageHeader userCtx =
|
||||
divAttr [ "class", "navbar navbar-inverse navbar-fixed-top" ] [
|
||||
divAttr [ "class", "container" ] [
|
||||
divAttr [ "class", "navbar-header" ] [
|
||||
buttonAttr [ "class", "navbar-toggle"; "data-toggle", "collapse"; "data-target", ".navbar-collapse" ] [
|
||||
spanAttr [ "class", "sr-only" ] (text "Toggle navigation")
|
||||
spanAttr [ "class", "icon-bar" ] noText
|
||||
spanAttr [ "class", "icon-bar" ] noText
|
||||
spanAttr [ "class", "icon-bar" ] noText
|
||||
]
|
||||
navLinkAttr [ "class", "navbar-brand" ] "/" "myPrayerJournal"
|
||||
]
|
||||
divAttr [ "class", "navbar-collapse collapse" ] [
|
||||
ulAttr [ "class", "nav navbar-nav navbar-right" ] (navigation userCtx)
|
||||
]
|
||||
]
|
||||
]
|
||||
let pageFooter =
|
||||
footerAttr [ "class", "mpj-footer" ] [
|
||||
pAttr [ "class", "text-right" ] [
|
||||
text "myPrayerJournal v0.8.1"
|
||||
]
|
||||
]
|
||||
let row = divAttr [ "class", "row" ]
|
||||
|
||||
let fullRow xml =
|
||||
row [ divAttr [ "class", "col-xs-12" ] xml ]
|
||||
|
||||
/// Display a page
|
||||
let page userCtx content =
|
||||
html [
|
||||
head [
|
||||
meta [ "charset", "UTF-8" ]
|
||||
meta [ "name", "viewport"; "content", "width=device-width, initial-scale=1" ]
|
||||
title "myPrayerJournal"
|
||||
stylesheet "https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
|
||||
stylesheet "/content/styles.css"
|
||||
stylesheet "https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
]
|
||||
body [
|
||||
pageHeader userCtx
|
||||
divAttr [ "class", "container body-content" ] [
|
||||
content
|
||||
pageFooter
|
||||
]
|
||||
js "https://cdn.auth0.com/js/lock/10.14/lock.min.js"
|
||||
js "/js/mpj.js"
|
||||
]
|
||||
]
|
||||
|> render
|
||||
|
||||
let home =
|
||||
fullRow [
|
||||
p [ text " "]
|
||||
p [ text "myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them, update them as God moves in the situation, and record a final answer received on that request. It will also allow individuals to review their answered prayers." ]
|
||||
p [ text "This site is currently in very limited alpha, as it is being developed with a core group of test users. If this is something you are interested in using, check back around mid-February 2017 to check on the development progress." ]
|
||||
]
|
||||
|
||||
let journal (reqs : Request list) =
|
||||
fullRow [
|
||||
p [ text "journal goes here" ]
|
||||
]
|
||||
35
src/api/wwwroot/content/styles.css
Normal file
35
src/api/wwwroot/content/styles.css
Normal file
@@ -0,0 +1,35 @@
|
||||
body {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 20px;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
|
||||
}
|
||||
|
||||
/* Wrapping element */
|
||||
/* Set some basic padding to keep content from hitting the edges */
|
||||
.body-content {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
.material-icons.md-18 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.material-icons.md-24 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.material-icons.md-36 {
|
||||
font-size: 36px;
|
||||
}
|
||||
.material-icons.md-48 {
|
||||
font-size: 48px;
|
||||
}
|
||||
.material-icons {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.mpj-page-title {
|
||||
border-bottom: solid 1px lightgray;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.mpj-footer {
|
||||
border-top: solid 1px lightgray;
|
||||
margin-top: 20px;
|
||||
}
|
||||
15
src/api/wwwroot/js/mpj.js
Normal file
15
src/api/wwwroot/js/mpj.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* myPrayerJournal script file
|
||||
*/
|
||||
var mpj = {
|
||||
lock: new Auth0Lock('Of2s0RQCQ3mt3dwIkOBY5h85J9sXbF2n', 'djs-consulting.auth0.com', {
|
||||
auth: {
|
||||
redirectUrl: 'http://localhost:8080/user/log-on',
|
||||
allowSignUp: false
|
||||
}
|
||||
}),
|
||||
|
||||
signIn: function() {
|
||||
this.lock.show()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user