mostly housekeeping

- App - some package clean-up
- API - removed commented code that is not needed, postured for JSON
outputs
- Build - modified the scripts to build Vue into API's wwwroot folder,
which will get copied on publish; adjusted FAKE task dependencies to be
more granular
This commit is contained in:
Daniel J. Summers 2017-08-06 15:42:09 -05:00
parent 196db30cc5
commit 5a7a74c167
13 changed files with 411 additions and 1003 deletions

3
.gitignore vendored
View File

@ -253,8 +253,7 @@ paket-files/
*.sln.iml *.sln.iml
# Compiled files / application # Compiled files / application
**/vendor-bundle.js
**/app-bundle.js*
src/api/wwwroot/index.html src/api/wwwroot/index.html
src/api/wwwroot/static
src/api/appsettings.json src/api/appsettings.json
build/ build/

View File

@ -1,5 +1,5 @@
@echo off @echo off
cd .\src\app cd .\src\app
au build npm run build prod
cd ..\.. cd ..\..
exit %errorlevel% exit %errorlevel%

View File

@ -4,7 +4,7 @@ open System
let buildDir = "./build/" let buildDir = "./build/"
/// Path to the Aurelia app /// Path to the Vue app
let appPath = "src" @@ "app" let appPath = "src" @@ "app"
/// Path to the Suave API /// Path to the Suave API
@ -14,27 +14,15 @@ let apiPath = "src" @@ "api"
Target "Clean" (fun _ -> Target "Clean" (fun _ ->
CleanDir buildDir CleanDir buildDir
CleanDir (apiPath @@ "wwwroot")
) )
Target "BuildApp" (fun _ -> Target "BuildApp" (fun _ ->
let result = let result =
ExecProcessAndReturnMessages (fun info -> ExecProcessAndReturnMessages (fun info ->
info.UseShellExecute <- false info.UseShellExecute <- false
info.FileName <- "." @@ "build-au.bat") (TimeSpan.FromMinutes 2.) info.FileName <- "build-vue.bat") (TimeSpan.FromMinutes 2.)
match result.ExitCode with match result.ExitCode with 0 -> Log "AppBuild-Output: " result.Messages | _ -> failwith "Vue build failed"
| 0 -> Log "AppBuild-Output: " result.Messages
| _ -> failwith "Aurelia build failed"
)
Target "CopyApp" (fun _ ->
let apiWebPath = apiPath @@ "wwwroot"
[ "scripts" @@ "app-bundle.js"
"scripts" @@ "vendor-bundle.js"
"index.html"
]
|> List.iter (fun file ->
IO.File.Copy (appPath @@ file, apiWebPath @@ file, true)
Log "CopyApp--Output: " (Seq.singleton file))
) )
Target "BuildApi" (fun _ -> Target "BuildApi" (fun _ ->
@ -43,26 +31,24 @@ Target "BuildApi" (fun _ ->
info.UseShellExecute <- false info.UseShellExecute <- false
info.FileName <- "dotnet" info.FileName <- "dotnet"
info.Arguments <- "build" info.Arguments <- "build"
info.WorkingDirectory <- "src" @@ "api") (TimeSpan.FromMinutes 2.) info.WorkingDirectory <- apiPath) (TimeSpan.FromMinutes 2.)
Log "AppBuild-Output: " result.Messages Log "AppBuild-Output: " result.Messages
match result.ExitCode with match result.ExitCode with 0 -> () | _ -> failwith "API build failed"
| 0 -> () )
| _ -> failwith "API build failed"
(*!! "src/api/*.fsproj" Target "Publish" (fun _ ->
|> MSBuildRelease buildDir "Build" ExecProcess (fun info ->
|> Log "ApiBuild-Output: " *) info.FileName <- "dotnet"
info.Arguments <- """publish -o ..\..\build"""
info.WorkingDirectory <- apiPath) TimeSpan.MaxValue
|> ignore
) )
Target "Run" (fun _ -> Target "Run" (fun _ ->
ExecProcess (fun info ->
info.FileName <- "dotnet"
info.Arguments <- """publish -o ..\..\build"""
info.WorkingDirectory <- "src" @@ "api") TimeSpan.MaxValue
|> ignore
ExecProcess (fun info -> ExecProcess (fun info ->
info.FileName <- "dotnet" info.FileName <- "dotnet"
info.Arguments <- "myPrayerJournal.dll" info.Arguments <- "myPrayerJournal.dll"
info.WorkingDirectory <- "build") TimeSpan.MaxValue info.WorkingDirectory <- buildDir) TimeSpan.MaxValue
|> ignore |> ignore
) )
@ -74,11 +60,18 @@ Target "Default" (fun _ ->
"Clean" "Clean"
==> "BuildApp" ==> "BuildApp"
==> "CopyApp"
"BuildApp"
==> "BuildApi" ==> "BuildApi"
==> "Default"
"BuildApi" "BuildApi"
==> "Publish"
"Publish"
==> "Run" ==> "Run"
"BuildApi"
==> "Default"
RunTargetOrDefault "Default" RunTargetOrDefault "Default"

View File

@ -1,76 +1,81 @@
/// Main server module for myPrayerJournal /// Main server module for myPrayerJournal
module MyPrayerJournal.App module MyPrayerJournal.App
open Auth0.AuthenticationApi
open Auth0.AuthenticationApi.Models
open Microsoft.EntityFrameworkCore open Microsoft.EntityFrameworkCore
open Newtonsoft.Json open Newtonsoft.Json
open Newtonsoft.Json.Linq open Newtonsoft.Json.Linq
open Reader
open System open System
open System.IO open System.IO
open Suave open Suave
open Suave.Filters open Suave.Filters
open Suave.Operators open Suave.Operators
open Suave.Redirection
open Suave.RequestErrors
open Suave.State.CookieStateStore
open Suave.Successful
let utf8 = System.Text.Encoding.UTF8 // --- Types ---
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)
/// Auth0 settings
type Auth0Config = { type Auth0Config = {
/// The domain used with Auth0
Domain : string Domain : string
/// The client Id
ClientId : string ClientId : string
/// The base64-encoded client secret
ClientSecret : string ClientSecret : string
} /// The URL-safe base64-encoded client secret
ClientSecretJwt : string
}
with with
/// An empty set of Auth0 settings
static member empty = static member empty =
{ Domain = "" { Domain = ""
ClientId = "" ClientId = ""
ClientSecret = "" ClientSecret = ""
ClientSecretJwt = ""
} }
/// Application configuration
type Config = { type Config = {
/// PostgreSQL connection string
Conn : string Conn : string
/// Auth0 settings
Auth0 : Auth0Config Auth0 : Auth0Config
} }
with with
static member empty = static member empty =
{ Conn = "" { Conn = ""
Auth0 = Auth0Config.empty Auth0 = Auth0Config.empty
} }
/// A JSON response as a data property
type JsonOkResponse<'a> = {
data : 'a
}
/// A JSON response indicating an error occurred
type JsonErrorResponse = {
error : string
}
// --- Support ---
/// Configuration instance
let cfg = let cfg =
try try
use sr = File.OpenText "appsettings.json" use sr = File.OpenText "appsettings.json"
let settings = JToken.ReadFrom(new JsonTextReader(sr)) :?> JObject use tr = new JsonTextReader (sr)
let settings = JToken.ReadFrom tr
let secret = settings.["auth0"].["client-secret"].ToObject<string>()
{ Conn = settings.["conn"].ToObject<string>() { Conn = settings.["conn"].ToObject<string>()
Auth0 = Auth0 =
{ Domain = settings.["auth0"].["domain"].ToObject<string>() { Domain = settings.["auth0"].["domain"].ToObject<string>()
ClientId = settings.["auth0"].["client-id"].ToObject<string>() ClientId = settings.["auth0"].["client-id"].ToObject<string>()
ClientSecret = settings.["auth0"].["client-secret"].ToObject<string>() ClientSecret = secret
ClientSecretJwt = secret.TrimEnd('=').Replace("-", "+").Replace("_", "/")
} }
} }
with _ -> Config.empty 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 /// Get the scheme, host, and port of the URL
let schemeHostPort (req : HttpRequest) = let schemeHostPort (req : HttpRequest) =
sprintf "%s://%s" req.url.Scheme (req.headers |> List.filter (fun x -> fst x = "host") |> List.head |> snd) sprintf "%s://%s" req.url.Scheme (req.headers |> List.filter (fun x -> fst x = "host") |> List.head |> snd)
@ -78,80 +83,37 @@ let schemeHostPort (req : HttpRequest) =
/// Authorization functions /// Authorization functions
module Auth = module Auth =
(* /// Shorthand for Console.WriteLine
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 let cw (x : string) = Console.WriteLine x
/// Convert microtime to ticks, add difference from 1/1/1 to 1/1/1970 /// Convert microtime to ticks, add difference from 1/1/1 to 1/1/1970
let jsDate jsTicks = let jsDate jsTicks =
DateTime(jsTicks * 10000000L).AddTicks(DateTime(1970, 1, 1).Ticks) DateTime(jsTicks * 10000000L).AddTicks(DateTime(1970, 1, 1).Ticks)
let getIdFromToken token = /// Get the user Id (sub) from a JSON Web Token
match token with let getIdFromToken jwt =
| Some jwt -> try
try let payload = Jose.JWT.Decode<JObject>(jwt, cfg.Auth0.ClientSecretJwt)
let key = Convert.FromBase64String(cfg.Auth0.ClientSecret.Replace("-", "+").Replace("_", "/")) let tokenExpires = jsDate (payload.["exp"].ToObject<int64>())
let payload = Jose.JWT.Decode<JObject>(jwt, key) match tokenExpires > DateTime.UtcNow with
let tokenExpires = jsDate (payload.["exp"].ToObject<int64>()) | true -> Some (payload.["sub"].ToObject<string>())
match tokenExpires > DateTime.UtcNow with | _ -> None
| true -> Some (payload.["sub"].ToObject<string>()) with ex ->
| _ -> None sprintf "Token Deserialization Exception - %s" (ex.GetType().FullName) |> cw
with ex -> sprintf "Message - %s" ex.Message |> cw
sprintf "Token Deserialization Exception - %s" (ex.GetType().FullName) |> cw ex.StackTrace |> cw
sprintf "Message - %s" ex.Message |> cw None
ex.StackTrace |> cw
None
| _ -> None
/// Add the logged on user Id to the context if it exists /// Add the logged on user Id to the context if it exists
let loggedOn = warbler (fun ctx -> let loggedOn =
match HttpContext.state ctx with warbler (fun ctx ->
| Some state -> Writers.setUserData "user" (state.get "auth-token" |> getIdFromToken) match ctx.request.header "Authorization" with
| Choice1Of2 bearer -> Writers.setUserData "user" ((bearer.Split(' ').[1]) |> getIdFromToken)
| _ -> Writers.setUserData "user" None) | _ -> Writers.setUserData "user" None)
/// Create a user context for the currently assigned user
//let userCtx ctx = { Id = ctx.userState.["user"] :?> string option } /// Serialize an object to JSON
let toJson = JsonConvert.SerializeObject
/// Read an item from the user state, downcast to the expected type /// Read an item from the user state, downcast to the expected type
let read ctx key : 'value = let read ctx key : 'value =
@ -161,37 +123,6 @@ let read ctx key : 'value =
let dataCtx () = let dataCtx () =
new DataContext (((DbContextOptionsBuilder<DataContext>()).UseNpgsql cfg.Conn).Options) 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))
let idx =
context (fun ctx ->
Console.WriteLine "serving index"
succeed)
/// Suave application
let app =
statefulForSession
>=> Auth.loggedOn
>=> choose [
path Route.home >=> Files.browseFileHome "index.html"
path Route.journal >=> viewJournal
//path Route.User.logOn >=> Auth.handleSignIn
//path Route.User.logOff >=> Auth.handleSignOut
Writers.setHeader "Cache-Control" "no-cache" >=> Files.browseHome
NOT_FOUND "Page not found."
]
/// Ensure the EF context is created in the right format /// Ensure the EF context is created in the right format
let ensureDatabase () = let ensureDatabase () =
async { async {
@ -204,16 +135,54 @@ let suaveCfg =
{ defaultConfig with { defaultConfig with
homeFolder = Some (Path.GetFullPath "./wwwroot/") homeFolder = Some (Path.GetFullPath "./wwwroot/")
serverKey = Text.Encoding.UTF8.GetBytes("12345678901234567890123456789012") serverKey = Text.Encoding.UTF8.GetBytes("12345678901234567890123456789012")
cookieSerialiser = JsonNetCookieSerializer () bindings = [ HttpBinding.createSimple HTTP "127.0.0.1" 8084 ]
} }
open Suave.Utils
// --- Routes ---
/// URL routes for myPrayerJournal
module Route =
/// /api/journal ~ All active prayer requests for a user
let journal = "/api/journal"
// --- WebParts ---
/// All WebParts that compose the public API
module WebParts =
let jsonMimeType =
warbler (fun ctx -> Writers.setMimeType "application/json; charset=utf8")
/// WebPart to return a JSON response
let JSON payload =
jsonMimeType
>=> Successful.OK (toJson { data = payload })
/// WebPart to return an JSON error response
let errorJSON code error =
jsonMimeType
>=> Writers.setStatus code
>=> Response.response code ((toJson >> UTF8.bytes) { error = error })
/// Journal page
let viewJournal =
context (fun ctx ->
use dataCtx = dataCtx ()
let reqs = Data.Requests.allForUser (defaultArg (read ctx "user") "") dataCtx
JSON reqs)
/// Suave application
let app =
Auth.loggedOn
>=> choose [
path Route.journal >=> viewJournal
errorJSON HttpCode.HTTP_404 "Page not found"
]
[<EntryPoint>] [<EntryPoint>]
let main argv = let main argv =
// Establish the data environment
//liftDep getConn (Data.establishEnvironment >> Async.RunSynchronously)
//|> run deps
ensureDatabase () ensureDatabase ()
startWebServer suaveCfg app startWebServer suaveCfg WebParts.app
0 0

View File

@ -16,7 +16,6 @@
<Compile Include="Data.fs" /> <Compile Include="Data.fs" />
<Compile Include="Migrations/20170104023341_InitialDb.fs" /> <Compile Include="Migrations/20170104023341_InitialDb.fs" />
<Compile Include="Migrations/DataContextModelSnapshot.fs" /> <Compile Include="Migrations/DataContextModelSnapshot.fs" />
<Compile Include="Route.fs" />
<Compile Include="App.fs" /> <Compile Include="App.fs" />
<None Update="appsettings.json"> <None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -28,9 +27,6 @@
<!-- Import Project="..\..\.paket\Paket.Restore.targets" / --> <!-- Import Project="..\..\.paket\Paket.Restore.targets" / -->
<ItemGroup> <ItemGroup>
<PackageReference Include="Auth0.AuthenticationApi">
<Version>4.1.0</Version>
</PackageReference>
<PackageReference Include="FSharp.Core"> <PackageReference Include="FSharp.Core">
<Version>4.1.17</Version> <Version>4.1.17</Version>
</PackageReference> </PackageReference>
@ -38,16 +34,16 @@
<Version>1.0.5</Version> <Version>1.0.5</Version>
</PackageReference> </PackageReference>
<PackageReference Include="jose-jwt"> <PackageReference Include="jose-jwt">
<Version>2.3.0</Version> <Version>2.*</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Newtonsoft.Json"> <PackageReference Include="Newtonsoft.Json">
<Version>10.0.2</Version> <Version>10.*</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL"> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL">
<Version>1.1.0</Version> <Version>1.*</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Suave"> <PackageReference Include="Suave">
<Version>2.1.0</Version> <Version>2.*</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>

View File

@ -1,15 +0,0 @@
/// 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"

View File

@ -1,35 +0,0 @@
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;
}

View File

@ -1,15 +0,0 @@
/**
* 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()
}
}

View File

@ -4,8 +4,8 @@ var path = require('path')
module.exports = { module.exports = {
build: { build: {
env: require('./prod.env'), env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'), index: path.resolve(__dirname, '../../api/wwwroot/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'), assetsRoot: path.resolve(__dirname, '../../api/wwwroot'),
assetsSubDirectory: 'static', assetsSubDirectory: 'static',
assetsPublicPath: '/', assetsPublicPath: '/',
productionSourceMap: true, productionSourceMap: true,

1027
src/app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@
}, },
"dependencies": { "dependencies": {
"auth0-js": "^8.8.0", "auth0-js": "^8.8.0",
"axios": "^0.16.2",
"element-ui": "^1.4.1", "element-ui": "^1.4.1",
"pug": "^2.0.0-rc.2", "pug": "^2.0.0-rc.2",
"vue": "^2.3.3", "vue": "^2.3.3",

View File

@ -19,9 +19,6 @@ export default {
</script> </script>
<style> <style>
@import url('../node_modules/bootstrap/dist/css/bootstrap.css');
@import url('../node_modules/bootstrap-vue/dist/bootstrap-vue.css');
body { body {
padding-top: 60px; padding-top: 60px;
} }

View File

@ -0,0 +1,9 @@
import axios from 'axios'
const http = axios.create({
baseURL: 'http://localhost:8084'
})
export default {
something: http.get('/blah')
}