myPrayerJournal v2 #27

Merged
danieljsummers merged 27 commits from version-2 into master 2019-09-03 00:01:26 +00:00
5 changed files with 138 additions and 88 deletions
Showing only changes of commit 7e08d7375f - Show all commits

View File

@ -93,6 +93,11 @@ export default {
}, },
hideProgress () { hideProgress () {
this.progress.visible = false this.progress.visible = false
},
handleLoginEvent (data) {
if (!data.loggedIn) {
this.showInfo('Logged out successfully')
}
} }
}, },
provide () { provide () {

View File

@ -7,30 +7,31 @@ import AUTH_CONFIG from './auth0-variables'
import mutations from '@/store/mutation-types' import mutations from '@/store/mutation-types'
/* es-lint-enable*/ /* es-lint-enable*/
// Auth0 web authentication instance to use for our calls
const webAuth = new auth0.WebAuth({ const webAuth = new auth0.WebAuth({
domain: AUTH_CONFIG.domain, domain: AUTH_CONFIG.domain,
clientID: AUTH_CONFIG.clientId, clientID: AUTH_CONFIG.clientId,
redirectUri: AUTH_CONFIG.appDomain + AUTH_CONFIG.callbackUrl, redirectUri: AUTH_CONFIG.appDomain + AUTH_CONFIG.callbackUrl,
audience: `https://${AUTH_CONFIG.domain}/userinfo`,
responseType: 'token id_token', responseType: 'token id_token',
scope: 'openid profile email' scope: 'openid profile email'
}) })
/**
* A class to handle all authentication calls and determinations
*/
class AuthService extends EventEmitter { class AuthService extends EventEmitter {
id = { // Local storage key for our session data
token: null, AUTH_SESSION = 'auth-session'
expiry: null
}
access = {
token: null,
expiry: null
}
profile = null
ACCESS_TOKEN = 'access_token' // Received and calculated values for our ssesion (initially loaded from local storage if present)
ID_TOKEN = 'id_token' session = {}
EXPIRES_AT = 'expires_at'
USER_PROFILE = 'user_profile' constructor() {
super()
this.refreshSession()
}
/** /**
* Starts the user log in flow * Starts the user log in flow
@ -56,31 +57,37 @@ class AuthService extends EventEmitter {
}) })
} }
handleAuthentication (store) { /**
this.parseHash() * Handle authentication replies from Auth0
.then(authResult => { *
* @param store The Vuex store
*/
async handleAuthentication (store) {
try {
const authResult = await this.parseHash()
if (authResult && authResult.accessToken && authResult.idToken) { if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult) this.setSession(authResult)
store.commit(mutations.USER_LOGGED_ON, this.profile) store.commit(mutations.USER_LOGGED_ON, this.session.profile)
} }
}) } catch(err) {
.catch(err => { console.error(err)
console.log(err)
alert(`Error: ${err.error}. Check the console for further details.`) alert(`Error: ${err.error}. Check the console for further details.`)
}) }
} }
/**
* Set up the session and commit it to local storage
*
* @param authResult The authorization result
*/
setSession (authResult) { setSession (authResult) {
this.profile = authResult.idTokenPayload this.session.profile = authResult.idTokenPayload
this.id.token = authResult.idToken this.session.id.token = authResult.idToken
this.id.expiry = new Date(this.profile.exp * 1000); this.session.id.expiry = this.session.profile.exp * 1000
this.access.token = authResult.accessToken this.session.access.token = authResult.accessToken
this.access.expiry = new Date(Date.now() + authResult.expiresIn * 1000) this.session.access.expiry = authResult.expiresIn * 1000 + Date.now()
localStorage.setItem(this.ACCESS_TOKEN, authResult.accessToken) localStorage.setItem(this.AUTH_SESSION, JSON.stringify(this.session))
localStorage.setItem(this.ID_TOKEN, authResult.idToken)
localStorage.setItem(this.EXPIRES_AT, this.id.expiry)
localStorage.setItem(this.USER_PROFILE, JSON.stringify(this.profile))
this.emit('loginEvent', { this.emit('loginEvent', {
loggedIn: true, loggedIn: true,
@ -89,9 +96,32 @@ class AuthService extends EventEmitter {
}) })
} }
/**
* Refresh this instance's session from the one in local storage
*/
refreshSession () {
this.session =
localStorage.getItem(this.AUTH_SESSION)
? JSON.parse(localStorage.getItem(this.AUTH_SESSION))
: { profile: {},
id: {
token: null,
expiry: null
},
access: {
token: null,
expiry: null
}
}
}
/**
* Renew authorzation tokens with Auth0
*/
renewTokens () { renewTokens () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (localStorage.getItem(this.ID_TOKEN) !== null) { this.refreshSession()
if (this.session.id.token !== null) {
webAuth.checkSession({}, (err, authResult) => { webAuth.checkSession({}, (err, authResult) => {
if (err) { if (err) {
reject(err) reject(err)
@ -106,45 +136,58 @@ class AuthService extends EventEmitter {
}) })
} }
logout (store, router) { /**
* Log out of myPrayerJournal
*
* @param store The Vuex store
*/
logout (store) {
// Clear access token and ID token from local storage // Clear access token and ID token from local storage
localStorage.removeItem(this.ACCESS_TOKEN) localStorage.removeItem(this.AUTH_SESSION)
localStorage.removeItem(this.ID_TOKEN) this.refreshSession()
localStorage.removeItem(this.EXPIRES_AT)
localStorage.removeItem(this.USER_PROFILE)
this.idToken = null
this.idTokenExpiry = null
this.profile = null
store.commit(mutations.USER_LOGGED_OFF) store.commit(mutations.USER_LOGGED_OFF)
webAuth.logout({ webAuth.logout({
// navigate to the home route returnTo: `${AUTH_CONFIG.appDomain}/`,
returnTo: '/' clientID: AUTH_CONFIG.clientId
}) })
this.emit('loginEvent', { loggedIn: false }) this.emit('loginEvent', { loggedIn: false })
} }
/**
* Check expiration for a token (the way it's stored in the session)
*/
checkExpiry = (it) => it.token && it.expiry && Date.now() < it.expiry
/**
* Is there a user authenticated?
*/
isAuthenticated () { isAuthenticated () {
return Date.now() < this.id.Expiry && localStorage.getItem(this.ID_TOKEN) return this.checkExpiry(this.session.id)
} }
/**
* Is the current access token valid?
*/
isAccessTokenValid () { isAccessTokenValid () {
return this.access.token && this.access.expiry && Date.now() < this.access.expiry return this.checkExpiry(this.session.access)
} }
getAccessToken () { /**
return new Promise((resolve, reject) => { * Get the user's access token, renewing it if required
*/
async getAccessToken () {
if (this.isAccessTokenValid()) { if (this.isAccessTokenValid()) {
resolve(this.access.token) return this.session.access.token
} else { } else {
this.renewTokens() try {
.then(authResult => { const authResult = await this.renewTokens()
resolve(authResult.accessToken) return authResult.accessToken
}, reject) } catch (reject) {
throw reject
}
} }
})
} }
} }

View File

@ -1,30 +1,29 @@
<template lang="pug"> <template lang="pug">
.md-toolbar-row .md-toolbar-row
md-tabs(md-sync-route).md-primary md-tabs(md-sync-route).md-primary
md-tab(v-if='isAuthenticated' template(v-if='isAuthenticated')
md-label='Journal' md-tab(md-label='Journal'
to='/journal') to='/journal')
md-tab(v-if='isAuthenticated' md-tab(md-label='Active'
md-label='Active'
to='/requests/active') to='/requests/active')
md-tab(v-if='hasSnoozed' md-tab(v-if='hasSnoozed'
md-label='Snoozed' md-label='Snoozed'
to='/requests/snoozed') to='/requests/snoozed')
md-tab(v-if='isAuthenticated' md-tab(md-label='Answered'
md-label='Answered'
to='/requests/answered') to='/requests/answered')
md-tab(v-if='isAuthenticated' md-tab(md-label='Log Off'
md-label='Log Off' href='/user/log-off'
href='#' @click.prevent='logOff()')
@click.stop='logOff()')
md-tab(v-if='!isAuthenticated'
md-label='Log On'
href='#'
@click.stop='logOn()')
md-tab(md-label='Docs' md-tab(md-label='Docs'
href='https://docs.prayerjournal.me' href='https://docs.prayerjournal.me'
target='_blank' @click.prevent='showHelp()')
@click.stop='') template(v-else)
md-tab(md-label='Log On'
href='/user/log-on'
@click.prevent='logOn()')
md-tab(md-label='Docs'
href='https://docs.prayerjournal.me'
@click.prevent='showHelp()')
</template> </template>
<script> <script>
@ -51,6 +50,9 @@ export default {
}, },
logOff () { logOff () {
this.$auth.logout(this.$store, this.$router) this.$auth.logout(this.$store, this.$router)
},
showHelp () {
window.open('https://docs.prayerjournal.me','_blank')
} }
} }
} }

View File

@ -10,9 +10,9 @@ article.mpj-main-content(role='main')
export default { export default {
name: 'log-on', name: 'log-on',
inject: ['progress'], inject: ['progress'],
created () { async created () {
this.progress.$emit('show', 'indeterminate') this.progress.$emit('show', 'indeterminate')
this.$auth.handleAuthentication(this.$store) await this.$auth.handleAuthentication(this.$store)
}, },
methods: { methods: {
handleLoginEvent (data) { handleLoginEvent (data) {

View File

@ -38,7 +38,7 @@ const logError = function (error) {
const setBearer = async function () { const setBearer = async function () {
try { try {
await auth.getAccessToken() await auth.getAccessToken()
api.setBearer(localStorage.getItem(auth.ID_TOKEN)) api.setBearer(auth.session.id.token)
} catch(err) { } catch(err) {
if (err === 'Not logged in') { if (err === 'Not logged in') {
console.warn('API request attempted when user was not logged in') console.warn('API request attempted when user was not logged in')
@ -50,7 +50,7 @@ const setBearer = async function () {
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
user: JSON.parse(localStorage.getItem(auth.USER_PROFILE) || '{}'), user: auth.session.profile,
isAuthenticated: auth.isAuthenticated(), isAuthenticated: auth.isAuthenticated(),
journal: {}, journal: {},
isLoadingJournal: false isLoadingJournal: false
@ -98,7 +98,7 @@ export default new Vuex.Store({
}, },
async [actions.CHECK_AUTHENTICATION] ({ commit }) { async [actions.CHECK_AUTHENTICATION] ({ commit }) {
try { try {
await auth.renewTokens() await auth.getAccessToken()
commit(mutations.SET_AUTHENTICATION, auth.isAuthenticated()) commit(mutations.SET_AUTHENTICATION, auth.isAuthenticated())
} catch(_) { } catch(_) {
commit(mutations.SET_AUTHENTICATION, false) commit(mutations.SET_AUTHENTICATION, false)