Moved Suave app to /api directory

This commit is contained in:
Daniel J. Summers
2017-05-04 21:45:30 -05:00
parent d5635ef37c
commit e522ab1ae1
12 changed files with 0 additions and 0 deletions

217
src/api/App.fs Normal file
View 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
View 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
View 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
View 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
View 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

View 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

View 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

View 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
View 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
View 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 "&nbsp;"]
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" ]
]

View 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
View 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()
}
}