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
# Compiled files / application
**/vendor-bundle.js
**/app-bundle.js*
src/api/wwwroot/index.html
src/api/wwwroot/static
src/api/appsettings.json
build/

View File

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

View File

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

View File

@ -1,76 +1,81 @@
/// 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)
// --- Types ---
/// Auth0 settings
type Auth0Config = {
/// The domain used with Auth0
Domain : string
/// The client Id
ClientId : string
/// The base64-encoded client secret
ClientSecret : string
}
/// The URL-safe base64-encoded client secret
ClientSecretJwt : string
}
with
/// An empty set of Auth0 settings
static member empty =
{ Domain = ""
ClientId = ""
ClientSecret = ""
ClientSecretJwt = ""
}
/// Application configuration
type Config = {
/// PostgreSQL connection string
Conn : string
/// Auth0 settings
Auth0 : Auth0Config
}
}
with
static member empty =
{ Conn = ""
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 =
try
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>()
Auth0 =
{ Domain = settings.["auth0"].["domain"].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
/// 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)
@ -78,120 +83,46 @@ let schemeHostPort (req : HttpRequest) =
/// Authorization functions
module Auth =
(*
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))) *)
/// Shorthand for Console.WriteLine
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
/// Get the user Id (sub) from a JSON Web Token
let getIdFromToken jwt =
try
let payload = Jose.JWT.Decode<JObject>(jwt, cfg.Auth0.ClientSecretJwt)
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
/// 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)
let loggedOn =
warbler (fun ctx ->
match ctx.request.header "Authorization" with
| Choice1Of2 bearer -> Writers.setUserData "user" ((bearer.Split(' ').[1]) |> getIdFromToken)
| _ -> 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
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))
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
let ensureDatabase () =
async {
@ -204,16 +135,54 @@ let suaveCfg =
{ defaultConfig with
homeFolder = Some (Path.GetFullPath "./wwwroot/")
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>]
let main argv =
// Establish the data environment
//liftDep getConn (Data.establishEnvironment >> Async.RunSynchronously)
//|> run deps
ensureDatabase ()
startWebServer suaveCfg app
startWebServer suaveCfg WebParts.app
0

View File

@ -16,7 +16,6 @@
<Compile Include="Data.fs" />
<Compile Include="Migrations/20170104023341_InitialDb.fs" />
<Compile Include="Migrations/DataContextModelSnapshot.fs" />
<Compile Include="Route.fs" />
<Compile Include="App.fs" />
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -28,9 +27,6 @@
<!-- Import Project="..\..\.paket\Paket.Restore.targets" / -->
<ItemGroup>
<PackageReference Include="Auth0.AuthenticationApi">
<Version>4.1.0</Version>
</PackageReference>
<PackageReference Include="FSharp.Core">
<Version>4.1.17</Version>
</PackageReference>
@ -38,16 +34,16 @@
<Version>1.0.5</Version>
</PackageReference>
<PackageReference Include="jose-jwt">
<Version>2.3.0</Version>
<Version>2.*</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>10.0.2</Version>
<Version>10.*</Version>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL">
<Version>1.1.0</Version>
<Version>1.*</Version>
</PackageReference>
<PackageReference Include="Suave">
<Version>2.1.0</Version>
<Version>2.*</Version>
</PackageReference>
</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 = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
index: path.resolve(__dirname, '../../api/wwwroot/index.html'),
assetsRoot: path.resolve(__dirname, '../../api/wwwroot'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
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": {
"auth0-js": "^8.8.0",
"axios": "^0.16.2",
"element-ui": "^1.4.1",
"pug": "^2.0.0-rc.2",
"vue": "^2.3.3",

View File

@ -19,9 +19,6 @@ export default {
</script>
<style>
@import url('../node_modules/bootstrap/dist/css/bootstrap.css');
@import url('../node_modules/bootstrap-vue/dist/bootstrap-vue.css');
body {
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')
}