Proof of concept on API calls...
...remains unproven.
This commit is contained in:
parent
5a7a74c167
commit
15947abcbf
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
28
src/app/src/api/index.js
Normal 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')
|
||||||
|
|
||||||
|
}
|
@ -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>
|
||||||
|
1
src/app/src/store/action-types.js
Normal file
1
src/app/src/store/action-types.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const LOAD_JOURNAL = 'load-journal'
|
@ -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: {}
|
||||||
})
|
})
|
||||||
|
@ -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'
|
||||||
|
Loading…
Reference in New Issue
Block a user