Proof of concept on API calls...

...remains unproven.
This commit is contained in:
Daniel J. Summers 2017-08-06 23:17:08 -05:00
parent 5a7a74c167
commit 15947abcbf
9 changed files with 135 additions and 51 deletions

View File

@ -33,7 +33,7 @@ with
} }
/// Application configuration /// Application configuration
type Config = { type AppConfig = {
/// PostgreSQL connection string /// PostgreSQL connection string
Conn : string Conn : string
/// Auth0 settings /// Auth0 settings
@ -45,40 +45,45 @@ with
Auth0 = Auth0Config.empty Auth0 = Auth0Config.empty
} }
/// A JSON response as a data property /// A JSON response as a data property
type JsonOkResponse<'a> = { type JsonOkResponse<'a> = {
data : 'a data : 'a
} }
/// A JSON response indicating an error occurred /// A JSON response indicating an error occurred
type JsonErrorResponse = { type JsonErrorResponse = {
error : string error : string
} }
// --- Support ---
/// Configuration instance /// Configuration instances
let cfg = module Config =
try
use sr = File.OpenText "appsettings.json" /// Application configuration
use tr = new JsonTextReader (sr) let app =
let settings = JToken.ReadFrom tr try
let secret = settings.["auth0"].["client-secret"].ToObject<string>() use sr = File.OpenText "appsettings.json"
{ Conn = settings.["conn"].ToObject<string>() use tr = new JsonTextReader (sr)
Auth0 = let settings = JToken.ReadFrom tr
{ Domain = settings.["auth0"].["domain"].ToObject<string>() let secret = settings.["auth0"].["client-secret"].ToObject<string>()
ClientId = settings.["auth0"].["client-id"].ToObject<string>() { Conn = settings.["conn"].ToObject<string>()
ClientSecret = secret Auth0 =
ClientSecretJwt = secret.TrimEnd('=').Replace("-", "+").Replace("_", "/") { Domain = settings.["auth0"].["domain"].ToObject<string>()
} ClientId = settings.["auth0"].["client-id"].ToObject<string>()
ClientSecret = secret
ClientSecretJwt = secret.TrimEnd('=').Replace("-", "+").Replace("_", "/")
}
}
with _ -> AppConfig.empty
/// Custom Suave configuration
let suave =
{ defaultConfig with
homeFolder = Some (Path.GetFullPath "./wwwroot/")
serverKey = Text.Encoding.UTF8.GetBytes("12345678901234567890123456789012")
bindings = [ HttpBinding.createSimple HTTP "127.0.0.1" 8084 ]
} }
with _ -> Config.empty
/// 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 /// Authorization functions
module Auth = module Auth =
@ -93,7 +98,7 @@ module Auth =
/// Get the user Id (sub) from a JSON Web Token /// Get the user Id (sub) from a JSON Web Token
let getIdFromToken jwt = let getIdFromToken jwt =
try try
let payload = Jose.JWT.Decode<JObject>(jwt, cfg.Auth0.ClientSecretJwt) let payload = Jose.JWT.Decode<JObject>(jwt, Config.app.Auth0.ClientSecretJwt)
let tokenExpires = jsDate (payload.["exp"].ToObject<int64>()) let tokenExpires = jsDate (payload.["exp"].ToObject<int64>())
match tokenExpires > DateTime.UtcNow with match tokenExpires > DateTime.UtcNow with
| true -> Some (payload.["sub"].ToObject<string>()) | true -> Some (payload.["sub"].ToObject<string>())
@ -108,10 +113,16 @@ module Auth =
let loggedOn = let loggedOn =
warbler (fun ctx -> warbler (fun ctx ->
match ctx.request.header "Authorization" with match ctx.request.header "Authorization" with
| Choice1Of2 bearer -> Writers.setUserData "user" ((bearer.Split(' ').[1]) |> getIdFromToken) | Choice1Of2 bearer -> Writers.setUserData "user" (getIdFromToken <| bearer.Split(' ').[1])
| _ -> Writers.setUserData "user" None) | _ -> Writers.setUserData "user" None)
// --- Support ---
/// 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)
/// Serialize an object to JSON /// Serialize an object to JSON
let toJson = JsonConvert.SerializeObject let toJson = JsonConvert.SerializeObject
@ -121,7 +132,7 @@ let read ctx key : 'value =
/// Create a new data context /// Create a new data context
let dataCtx () = let dataCtx () =
new DataContext (((DbContextOptionsBuilder<DataContext>()).UseNpgsql cfg.Conn).Options) new DataContext (((DbContextOptionsBuilder<DataContext>()).UseNpgsql Config.app.Conn).Options)
/// Ensure the EF context is created in the right format /// Ensure the EF context is created in the right format
let ensureDatabase () = let ensureDatabase () =
@ -131,14 +142,6 @@ let ensureDatabase () =
} }
|> Async.RunSynchronously |> Async.RunSynchronously
let suaveCfg =
{ defaultConfig with
homeFolder = Some (Path.GetFullPath "./wwwroot/")
serverKey = Text.Encoding.UTF8.GetBytes("12345678901234567890123456789012")
bindings = [ HttpBinding.createSimple HTTP "127.0.0.1" 8084 ]
}
// --- Routes ---
/// URL routes for myPrayerJournal /// URL routes for myPrayerJournal
module Route = module Route =
@ -147,8 +150,6 @@ module Route =
let journal = "/api/journal" let journal = "/api/journal"
// --- WebParts ---
/// All WebParts that compose the public API /// All WebParts that compose the public API
module WebParts = module WebParts =
@ -163,7 +164,6 @@ module WebParts =
/// WebPart to return an JSON error response /// WebPart to return an JSON error response
let errorJSON code error = let errorJSON code error =
jsonMimeType jsonMimeType
>=> Writers.setStatus code
>=> Response.response code ((toJson >> UTF8.bytes) { error = error }) >=> Response.response code ((toJson >> UTF8.bytes) { error = error })
/// Journal page /// Journal page
@ -177,12 +177,12 @@ module WebParts =
let app = let app =
Auth.loggedOn Auth.loggedOn
>=> choose [ >=> choose [
path Route.journal >=> viewJournal GET >=> path Route.journal >=> viewJournal
errorJSON HttpCode.HTTP_404 "Page not found" errorJSON HttpCode.HTTP_404 "Page not found"
] ]
[<EntryPoint>] [<EntryPoint>]
let main argv = let main argv =
ensureDatabase () ensureDatabase ()
startWebServer suaveCfg WebParts.app startWebServer Config.suave WebParts.app
0 0

View File

@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>myPrayerJournal</title> <title>myPrayerJournal</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<!-- link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css" -->
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -19,8 +19,14 @@ export default {
</script> </script>
<style> <style>
@import url('../node_modules/element-ui/lib/theme-default/index.css');
body { body {
font-family: -apple-system,system-ui,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue", Arial, sans-serif;
padding-top: 60px; padding-top: 60px;
margin: 0;
}
#content {
padding: 0 10px;
} }
footer { footer {
border-top: solid 1px lightgray; border-top: solid 1px lightgray;
@ -28,6 +34,12 @@ footer {
padding: 0 1rem; padding: 0 1rem;
} }
footer p {
margin: 0;
}
.text-right {
text-align: right;
}
.material-icons.md-18 { .material-icons.md-18 {
font-size: 18px; font-size: 18px;
} }

View File

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

28
src/app/src/api/index.js Normal file
View File

@ -0,0 +1,28 @@
import axios from 'axios'
const http = axios.create({
baseURL: 'http://localhost:8084/api'
})
/**
* API access for myPrayerJournal
*/
export default {
/**
* Set the bearer token for all future requests
* @param {string} token The token to use to identify the user to the server
*/
setBearer: token => { http.defaults.headers.common['Authentication'] = `Bearer ${token}` },
/**
* Remove the bearer token
*/
removeBearer: () => delete http.defaults.headers.common['Authentication'],
/**
* Get all prayer requests and their most recent updates
*/
journal: () => http.get('/journal')
}

View File

@ -2,14 +2,22 @@
article article
page-title(:title="title") page-title(:title="title")
p here you are! p here you are!
p(v-if="isLoadingJournal") journal is loading...
p(v-if="!isLoadingJournal") journal has {{ journal.length }} entries
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import PageTitle from './PageTitle' import PageTitle from './PageTitle'
import * as actions from '@/store/action-types'
export default { export default {
name: 'dashboard', name: 'dashboard',
data () {
this.$store.dispatch(actions.LOAD_JOURNAL)
return {}
},
components: { components: {
PageTitle PageTitle
}, },
@ -17,7 +25,7 @@ export default {
title () { title () {
return `${this.user.given_name}'s Dashboard` return `${this.user.given_name}'s Dashboard`
}, },
...mapState(['user']) ...mapState(['user', 'journal', 'isLoadingJournal'])
} }
} }
</script> </script>

View File

@ -0,0 +1 @@
export const LOAD_JOURNAL = 'load-journal'

View File

@ -1,18 +1,41 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import api from '@/api'
import AuthService from '@/auth/AuthService' import AuthService from '@/auth/AuthService'
import * as types from './mutation-types' import * as types from './mutation-types'
import * as actions from './action-types'
Vue.use(Vuex) Vue.use(Vuex)
this.auth0 = new AuthService() this.auth0 = new AuthService()
const logError = function (error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data)
console.log(error.response.status)
console.log(error.response.headers)
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log(error.request)
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message)
}
console.log(error.config)
}
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
user: JSON.parse(localStorage.getItem('user_profile') || '{}'), user: JSON.parse(localStorage.getItem('user_profile') || '{}'),
isAuthenticated: this.auth0.isAuthenticated() isAuthenticated: this.auth0.isAuthenticated(),
journal: {},
isLoadingJournal: false
}, },
mutations: { mutations: {
[types.USER_LOGGED_ON] (state, user) { [types.USER_LOGGED_ON] (state, user) {
@ -23,9 +46,29 @@ export default new Vuex.Store({
[types.USER_LOGGED_OFF] (state) { [types.USER_LOGGED_OFF] (state) {
state.user = {} state.user = {}
state.isAuthenticated = false state.isAuthenticated = false
},
[types.LOADING_JOURNAL] (state, flag) {
state.isLoadingJournal = flag
},
[types.LOADED_JOURNAL] (state, journal) {
state.journal = journal
}
},
actions: {
[actions.LOAD_JOURNAL] ({ commit }) {
commit(types.LOADED_JOURNAL, {})
commit(types.LOADING_JOURNAL, true)
api.journal()
.then(jrnl => {
commit(types.LOADING_JOURNAL, false)
commit(types.LOADED_JOURNAL, jrnl)
})
.catch(err => {
commit(types.LOADING_JOURNAL, false)
logError(err)
})
} }
}, },
actions: {},
getters: {}, getters: {},
modules: {} modules: {}
}) })

View File

@ -1,2 +1,4 @@
export const USER_LOGGED_OFF = 'user-logged-out' export const USER_LOGGED_OFF = 'user-logged-out'
export const USER_LOGGED_ON = 'user-logged-on' export const USER_LOGGED_ON = 'user-logged-on'
export const LOADING_JOURNAL = 'loading-journal'
export const LOADED_JOURNAL = 'journal-loaded'