WIP on migrating Vue 2 comp API to Vue 3
This commit is contained in:
parent
ea29f0572f
commit
b819b5e600
1250
src/MyPrayerJournal/App/package-lock.json
generated
1250
src/MyPrayerJournal/App/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -13,13 +13,19 @@
|
||||||
"vue": "vue-cli-service build --modern && cd ../Api && dotnet run"
|
"vue": "vue-cli-service build --modern && cd ../Api && dotnet run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^6.4.1",
|
||||||
|
"auth0-js": "^9.16.4",
|
||||||
|
"bootstrap": "^5.1.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"date-fns": "^2.24.0",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"vue-class-component": "^8.0.0-0",
|
"vue-class-component": "^8.0.0-0",
|
||||||
"vue-router": "^4.0.0-0",
|
"vue-router": "^4.0.0-0",
|
||||||
"vuex": "^4.0.0-0"
|
"vuex": "^4.0.0-0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/auth0-js": "^9.14.5",
|
||||||
|
"@types/events": "^3.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
||||||
"@typescript-eslint/parser": "^4.18.0",
|
"@typescript-eslint/parser": "^4.18.0",
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
@ -34,6 +40,7 @@
|
||||||
"eslint-plugin-vue": "^7.0.0",
|
"eslint-plugin-vue": "^7.0.0",
|
||||||
"sass": "^1.26.5",
|
"sass": "^1.26.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"typescript": "~4.1.5"
|
"typescript": "~4.1.5",
|
||||||
|
"vue-cli-plugin-pug": "~2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,38 @@
|
||||||
<template>
|
<template lang="pug">
|
||||||
<div id="nav">
|
.mpj-app
|
||||||
<router-link to="/">Home</router-link> |
|
#nav
|
||||||
<router-link to="/about">About</router-link>
|
router-link(to="/") Home
|
||||||
</div>
|
| |
|
||||||
<router-view/>
|
router-link(to="/about") About
|
||||||
|
router-view
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<script setup lang="ts">
|
||||||
#app {
|
import { provide } from "vue"
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav {
|
import "bootstrap/dist/css/bootstrap.min.css"
|
||||||
padding: 30px;
|
|
||||||
|
|
||||||
a {
|
import { AuthService } from "./auth"
|
||||||
font-weight: bold;
|
|
||||||
color: #2c3e50;
|
|
||||||
|
|
||||||
&.router-link-exact-active {
|
/** The auth service injection symbol */
|
||||||
color: #42b983;
|
export const AuthSymbol = Symbol("AuthService")
|
||||||
}
|
|
||||||
}
|
provide(AuthSymbol, AuthService)
|
||||||
}
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
#app
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif
|
||||||
|
-webkit-font-smoothing: antialiased
|
||||||
|
-moz-osx-font-smoothing: grayscale
|
||||||
|
text-align: center
|
||||||
|
color: #2c3e50
|
||||||
|
#nav
|
||||||
|
padding: 30px
|
||||||
|
a
|
||||||
|
font-weight: bold
|
||||||
|
color: #2c3e50
|
||||||
|
&.router-link-exact-active
|
||||||
|
color: #42b983
|
||||||
</style>
|
</style>
|
||||||
|
|
174
src/MyPrayerJournal/App/src/api/index.ts
Normal file
174
src/MyPrayerJournal/App/src/api/index.ts
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
import { JournalRequest, NotesEntry } from "./types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a URL that will access the API
|
||||||
|
* @param url The partial URL for the API
|
||||||
|
* @returns A full URL for the API
|
||||||
|
*/
|
||||||
|
const apiUrl = (url : string) : string => `/api/${url}`
|
||||||
|
|
||||||
|
/** The bearer token */
|
||||||
|
let bearer : string | undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create request init parameters
|
||||||
|
*
|
||||||
|
* @param method The method by which the request should be executed
|
||||||
|
* @param user The currently logged-on user
|
||||||
|
* @returns RequestInit parameters
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const reqInit = (method : string, body : any | undefined = undefined) : RequestInit => {
|
||||||
|
const headers = new Headers()
|
||||||
|
if (bearer) headers.append("Authorization", bearer)
|
||||||
|
if (body) {
|
||||||
|
headers.append("Content-Type", "application/json")
|
||||||
|
return {
|
||||||
|
headers,
|
||||||
|
method,
|
||||||
|
cache: "no-cache",
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
headers,
|
||||||
|
method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a result for an API call
|
||||||
|
*
|
||||||
|
* @param resp The response received from the API
|
||||||
|
* @param action The action being performed (used in error messages)
|
||||||
|
* @returns The expected result (if found), undefined (if not found), or an error string
|
||||||
|
*/
|
||||||
|
async function apiResult<T> (resp : Response, action : string) : Promise<T | undefined | string> {
|
||||||
|
if (resp.status === 200) return await resp.json() as T
|
||||||
|
if (resp.status === 404) return undefined
|
||||||
|
return `Error ${action} - ${await resp.text()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run an API action that does not return a result
|
||||||
|
*
|
||||||
|
* @param resp The response received from the API call
|
||||||
|
* @param action The action being performed (used in error messages)
|
||||||
|
* @returns Undefined (if successful), or an error string
|
||||||
|
*/
|
||||||
|
const apiAction = async (resp : Response, action : string) : Promise<string | undefined> => {
|
||||||
|
if (resp.status === 200) return undefined
|
||||||
|
return `Error ${action} - ${await resp.text()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API access for myPrayerJournal
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the bearer token for all future requests
|
||||||
|
* @param token The token to use to identify the user to the server
|
||||||
|
*/
|
||||||
|
setBearer: (token: string) => { bearer = `Bearer ${token}` },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the bearer token
|
||||||
|
*/
|
||||||
|
removeBearer: () => { bearer = undefined },
|
||||||
|
|
||||||
|
notes: {
|
||||||
|
/**
|
||||||
|
* Add a note for a prayer request
|
||||||
|
* @param requestId The Id of the request to which the note applies
|
||||||
|
* @param notes The notes to be added
|
||||||
|
*/
|
||||||
|
add: async (requestId : string, notes : string) =>
|
||||||
|
apiAction(await fetch(apiUrl(`request/${requestId}/note`), reqInit("POST", { notes })), "adding note"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get past notes for a prayer request
|
||||||
|
* @param requestId The Id of the request for which notes should be retrieved
|
||||||
|
*/
|
||||||
|
getForRequest: async (requestId : string) =>
|
||||||
|
apiResult<NotesEntry[]>(await fetch(apiUrl(`request/${requestId}/notes`), reqInit("GET")), "getting notes")
|
||||||
|
},
|
||||||
|
|
||||||
|
request: {
|
||||||
|
/**
|
||||||
|
* Add a new prayer request
|
||||||
|
* @param requestText The text of the request to be added
|
||||||
|
* @param recurType The type of recurrence for this request
|
||||||
|
* @param recurCount The number of intervals of recurrence
|
||||||
|
*/
|
||||||
|
add: async (requestText : string, recurType : string, recurCount : number) =>
|
||||||
|
apiAction(await fetch(apiUrl("request"), reqInit("POST", { requestText, recurType, recurCount })),
|
||||||
|
"adding prayer request"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a prayer request (full; includes all history and notes)
|
||||||
|
* @param requestId The Id of the request to retrieve
|
||||||
|
*/
|
||||||
|
full: async (requestId : string) =>
|
||||||
|
apiResult<JournalRequest>(await fetch(apiUrl(`request/${requestId}/full`), reqInit("GET")),
|
||||||
|
"retrieving full request"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a prayer request (journal-style; only latest update)
|
||||||
|
* @param requestId The Id of the request to retrieve
|
||||||
|
*/
|
||||||
|
get: async (requestId : string) =>
|
||||||
|
apiResult<JournalRequest>(await fetch(apiUrl(`request/${requestId}`), reqInit("GET")), "retrieving request"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all answered requests, along with the text they had when it was answered
|
||||||
|
*/
|
||||||
|
getAnswered: async () =>
|
||||||
|
apiResult<JournalRequest[]>(await fetch(apiUrl("requests/answered"), reqInit("GET")),
|
||||||
|
"retrieving answered requests"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all prayer requests and their most recent updates
|
||||||
|
*/
|
||||||
|
journal: async () =>
|
||||||
|
apiResult<JournalRequest[]>(await fetch(apiUrl('journal'), reqInit("GET")), "retrieving journal"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a request after the given date (used for "show now")
|
||||||
|
* @param requestId The ID of the request which should be shown
|
||||||
|
* @param showAfter The ticks after which the request should be shown
|
||||||
|
*/
|
||||||
|
show: async (requestId : string, showAfter : number) =>
|
||||||
|
apiAction(await fetch(apiUrl(`request/${requestId}/show`), reqInit("PATCH", { showAfter })), "showing request"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snooze a request until the given time
|
||||||
|
* @param requestId The ID of the prayer request to be snoozed
|
||||||
|
* @param until The ticks until which the request should be snoozed
|
||||||
|
*/
|
||||||
|
snooze: async (requestId : string, until : number) =>
|
||||||
|
apiAction(await fetch(apiUrl(`request/${requestId}/snooze`), reqInit("PATCH", { until })), "snoozing request"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a prayer request
|
||||||
|
* @param requestId The ID of the request to be updated
|
||||||
|
* @param status The status of the update
|
||||||
|
* @param updateText The text of the update (optional)
|
||||||
|
*/
|
||||||
|
update: async (requestId : string, status : string, updateText : string) =>
|
||||||
|
apiAction(await fetch(apiUrl(`request/${requestId}/history`), reqInit("POST", { status, updateText })),
|
||||||
|
"updating request"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update recurrence for a prayer request
|
||||||
|
* @param requestId The ID of the prayer request for which recurrence is being updated
|
||||||
|
* @param recurType The type of recurrence to set
|
||||||
|
* @param recurCount The number of recurrence intervals to set
|
||||||
|
*/
|
||||||
|
updateRecurrence: async (requestId : string, recurType : string, recurCount : number) =>
|
||||||
|
apiAction(await fetch(apiUrl(`request/${requestId}/recurrence`), reqInit("PATCH", { recurType, recurCount })),
|
||||||
|
"updating request recurrence")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './types'
|
43
src/MyPrayerJournal/App/src/api/types.ts
Normal file
43
src/MyPrayerJournal/App/src/api/types.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/** A history entry for a prayer request */
|
||||||
|
export class HistoryEntry {
|
||||||
|
/** The status for this history entry */
|
||||||
|
status = "" // TODO string union?
|
||||||
|
/** The date/time for this history entry */
|
||||||
|
asOf = 0
|
||||||
|
/** The text of this history entry */
|
||||||
|
text?: string = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An entry with notes for a request */
|
||||||
|
export class NotesEntry {
|
||||||
|
/** The date/time the notes were recorded */
|
||||||
|
asOf = 0
|
||||||
|
/** The notes */
|
||||||
|
notes = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A prayer request that is part of the user's journal */
|
||||||
|
export class JournalRequest {
|
||||||
|
/** The ID of the request (just the CUID part) */
|
||||||
|
requestId = ""
|
||||||
|
/** The ID of the user to whom the request belongs */
|
||||||
|
userId = ""
|
||||||
|
/** The current text of the request */
|
||||||
|
text = ""
|
||||||
|
/** The last time action was taken on the request */
|
||||||
|
asOf = 0
|
||||||
|
/** The last status for the request */
|
||||||
|
lastStatus = "" // TODO string union?
|
||||||
|
/** The time that this request should reappear in the user's journal */
|
||||||
|
snoozedUntil = 0
|
||||||
|
/** The time after which this request should reappear in the user's journal by configured recurrence */
|
||||||
|
showAfter = 0
|
||||||
|
/** The type of recurrence for this request */
|
||||||
|
recurType = "" // TODO Recurrence union?
|
||||||
|
/** How many of the recurrence intervals should occur between appearances in the journal */
|
||||||
|
recurCount = 0
|
||||||
|
/** History entries for the request */
|
||||||
|
history: HistoryEntry[] = []
|
||||||
|
/** Note entries for the request */
|
||||||
|
notes: NotesEntry[] = []
|
||||||
|
}
|
8
src/MyPrayerJournal/App/src/auth/auth0-variables.ts
Executable file
8
src/MyPrayerJournal/App/src/auth/auth0-variables.ts
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
export default {
|
||||||
|
clientId: 'Of2s0RQCQ3mt3dwIkOBY5h85J9sXbF2n',
|
||||||
|
domain: 'djs-consulting.auth0.com',
|
||||||
|
appDomain: `${location.protocol}//${location.host}`,
|
||||||
|
callbackUrl: '/user/log-on',
|
||||||
|
audience: 'https://prayerjournal.me'
|
||||||
|
}
|
184
src/MyPrayerJournal/App/src/auth/index.ts
Normal file
184
src/MyPrayerJournal/App/src/auth/index.ts
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import { Store } from "vuex"
|
||||||
|
import auth0, { Auth0DecodedHash } from "auth0-js"
|
||||||
|
import { EventEmitter } from "events"
|
||||||
|
|
||||||
|
import { Mutations, State } from "@/store"
|
||||||
|
|
||||||
|
import Auth0Config from "./auth0-variables"
|
||||||
|
import { Session, Token } from './types'
|
||||||
|
|
||||||
|
// Auth0 web authentication instance to use for our calls
|
||||||
|
const webAuth = new auth0.WebAuth({
|
||||||
|
domain: Auth0Config.domain,
|
||||||
|
clientID: Auth0Config.clientId,
|
||||||
|
redirectUri: Auth0Config.appDomain + Auth0Config.callbackUrl,
|
||||||
|
audience: `https://${Auth0Config.domain}/userinfo`,
|
||||||
|
responseType: "token id_token",
|
||||||
|
scope: "openid profile email"
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to handle all authentication calls and determinations
|
||||||
|
*/
|
||||||
|
export class AuthService extends EventEmitter {
|
||||||
|
// Local storage key for our session data
|
||||||
|
AUTH_SESSION = "auth-session"
|
||||||
|
|
||||||
|
// Received and calculated values for our ssesion (initially loaded from local storage if present)
|
||||||
|
session = new Session()
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
super()
|
||||||
|
this.refreshSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the user log in flow
|
||||||
|
* @param customState Application state to be returned after user has authenticated
|
||||||
|
*/
|
||||||
|
login (customState? : any) {
|
||||||
|
webAuth.authorize({
|
||||||
|
appState: customState
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promisified parseHash function
|
||||||
|
* @returns A promise that resolves with the parsed hash returned from Auth0
|
||||||
|
*/
|
||||||
|
parseHash () : Promise<Auth0DecodedHash> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
webAuth.parseHash((err, authResult) => {
|
||||||
|
if (err || authResult === null) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(authResult)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle authentication replies from Auth0
|
||||||
|
* @param store The Vuex store
|
||||||
|
*/
|
||||||
|
async handleAuthentication (store : Store<State>) {
|
||||||
|
try {
|
||||||
|
const authResult = await this.parseHash()
|
||||||
|
if (authResult && authResult.accessToken && authResult.idToken) {
|
||||||
|
this.setSession(authResult)
|
||||||
|
store.commit(Mutations.UserLoggedOn, this.session.profile)
|
||||||
|
}
|
||||||
|
} catch (err : any) {
|
||||||
|
console.error(err) // eslint-disable-line no-console
|
||||||
|
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 : Auth0DecodedHash) {
|
||||||
|
this.session.profile = authResult.idTokenPayload
|
||||||
|
this.session.id = new Token(authResult.idToken!, this.session.profile.exp * 1000)
|
||||||
|
this.session.access = new Token(authResult.accessToken!, authResult.expiresIn! * 1000 + Date.now())
|
||||||
|
|
||||||
|
localStorage.setItem(this.AUTH_SESSION, JSON.stringify(this.session))
|
||||||
|
|
||||||
|
this.emit("loginEvent", {
|
||||||
|
loggedIn: true,
|
||||||
|
profile: authResult.idTokenPayload,
|
||||||
|
state: authResult.appState || {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) || "{}")
|
||||||
|
: new Session()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renew authorzation tokens with Auth0
|
||||||
|
* @returns A promise with the parsed hash from the Auth0 response
|
||||||
|
*/
|
||||||
|
renewTokens () : Promise<Auth0DecodedHash> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.refreshSession()
|
||||||
|
if (this.session.id.token !== null) {
|
||||||
|
webAuth.checkSession({}, (err, authResult) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
const result = authResult as Auth0DecodedHash
|
||||||
|
this.setSession(result)
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
reject(new Error("Not logged in"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log out of myPrayerJournal
|
||||||
|
* @param store The Vuex store
|
||||||
|
*/
|
||||||
|
logout (store : Store<State>) {
|
||||||
|
// Clear access token and ID token from local storage
|
||||||
|
localStorage.removeItem(this.AUTH_SESSION)
|
||||||
|
this.refreshSession()
|
||||||
|
|
||||||
|
store.commit(Mutations.UserLoggedOff)
|
||||||
|
|
||||||
|
webAuth.logout({
|
||||||
|
returnTo: `${Auth0Config.appDomain}/`,
|
||||||
|
clientID: Auth0Config.clientId
|
||||||
|
})
|
||||||
|
this.emit("loginEvent", { loggedIn: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the given token is currently valid
|
||||||
|
* @param t The token to be validated
|
||||||
|
* @returns True if the token is valid
|
||||||
|
*/
|
||||||
|
isTokenValid = (t : Token) => t.token !== "" && t.expiry !== 0 && Date.now() < t.expiry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is there a user authenticated?
|
||||||
|
* @returns True if a user is authenticated
|
||||||
|
*/
|
||||||
|
isAuthenticated () {
|
||||||
|
return this.session?.id && this.isTokenValid(this.session.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the current access token valid?
|
||||||
|
* @returns True if the user's access token is valid
|
||||||
|
*/
|
||||||
|
isAccessTokenValid () {
|
||||||
|
return this.session?.access && this.isTokenValid(this.session.access)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user's access token, renewing it if required
|
||||||
|
* @returns A promise that resolves to the user's access token
|
||||||
|
*/
|
||||||
|
async getAccessToken () : Promise<string> {
|
||||||
|
if (this.isAccessTokenValid()) {
|
||||||
|
return this.session.access.token
|
||||||
|
} else {
|
||||||
|
const authResult = await this.renewTokens()
|
||||||
|
return authResult.accessToken!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AuthService()
|
19
src/MyPrayerJournal/App/src/auth/types.ts
Normal file
19
src/MyPrayerJournal/App/src/auth/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/** A token and expiration set */
|
||||||
|
export class Token {
|
||||||
|
/**
|
||||||
|
* Create a new token
|
||||||
|
* @param token The actual token
|
||||||
|
* @param expiry The expiration for the token
|
||||||
|
*/
|
||||||
|
constructor (public token: string, public expiry: number) { } // eslint-disable-line no-useless-constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A user's current session */
|
||||||
|
export class Session {
|
||||||
|
/** The user's profile from Auth0 */
|
||||||
|
profile: any = {}
|
||||||
|
/** The user's ID token */
|
||||||
|
id = new Token("", 0)
|
||||||
|
/** The user's access token */
|
||||||
|
access = new Token("", 0)
|
||||||
|
}
|
49
src/MyPrayerJournal/App/src/components/DateFromNow.vue
Normal file
49
src/MyPrayerJournal/App/src/components/DateFromNow.vue
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, h, onBeforeUnmount, onMounted, ref } from "vue"
|
||||||
|
import { format, formatDistance } from "date-fns"
|
||||||
|
|
||||||
|
/** Properties for this component */
|
||||||
|
interface Props {
|
||||||
|
/** The tag name to be rendered (defaults to `span`) */
|
||||||
|
tag : string
|
||||||
|
/** The value of the date */
|
||||||
|
value : number
|
||||||
|
/** The interval at which the date should be refreshed (defaults to 10 seconds) */
|
||||||
|
interval : number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
tag : "span",
|
||||||
|
value : 0,
|
||||||
|
interval : 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Interval ID for updating relative time */
|
||||||
|
let intervalId: number = 0
|
||||||
|
|
||||||
|
/** The relative time string */
|
||||||
|
const fromNow = ref(formatDistance(props.value, Date.now(), { addSuffix: true }))
|
||||||
|
|
||||||
|
/** The actual date/time (used as the title for the relative time) */
|
||||||
|
const actual = computed(() => format(props.value, 'PPPPp'))
|
||||||
|
|
||||||
|
/** Update the relative time string if it is different */
|
||||||
|
const updateFromNow = () => {
|
||||||
|
const newFromNow = formatDistance(props.value, Date.now(), { addSuffix: true })
|
||||||
|
if (newFromNow !== fromNow.value) fromNow.value = newFromNow
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Refresh the relative time string to keep it accurate */
|
||||||
|
onMounted(() => { intervalId = setInterval(updateFromNow, props.interval) })
|
||||||
|
|
||||||
|
/** Cancel refreshing the time string represented with this component */
|
||||||
|
onBeforeUnmount(() => clearInterval(intervalId))
|
||||||
|
|
||||||
|
/** Render the element */
|
||||||
|
h(props.tag, {
|
||||||
|
domProps: {
|
||||||
|
title: actual.value,
|
||||||
|
innerText: fromNow.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -1,65 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br>
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
|
||||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
|
||||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
|
||||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
|
||||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
|
||||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
|
||||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Options, Vue } from 'vue-class-component';
|
|
||||||
|
|
||||||
@Options({
|
|
||||||
props: {
|
|
||||||
msg: String
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class HelloWorld extends Vue {
|
|
||||||
msg!: string
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped lang="scss">
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
49
src/MyPrayerJournal/App/src/components/NavigationBar.vue
Normal file
49
src/MyPrayerJournal/App/src/components/NavigationBar.vue
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<template lang="pug">
|
||||||
|
.md-toolbar-row
|
||||||
|
md-tabs(md-sync-route).md-primary
|
||||||
|
template(v-if="isAuthenticated")
|
||||||
|
md-tab(md-label="Journal" to="/journal")
|
||||||
|
md-tab(md-label="Active" to="/requests/active")
|
||||||
|
md-tab(v-if="hasSnoozed" md-label="Snoozed" to="/requests/snoozed")
|
||||||
|
md-tab(md-label="Answered" to="/requests/answered")
|
||||||
|
md-tab(md-label="Log Off" href="/user/log-off" @click.prevent="logOff()")
|
||||||
|
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>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject } from "vue"
|
||||||
|
import { useRouter } from "vue-router"
|
||||||
|
|
||||||
|
import { useStore } from "@/store"
|
||||||
|
import { AuthService } from "@/auth"
|
||||||
|
import { AuthSymbol } from "@/App.vue"
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
/** The auth service */
|
||||||
|
const auth = inject(AuthSymbol) as AuthService
|
||||||
|
|
||||||
|
/** Whether a user is authenticated */
|
||||||
|
const isAuthenticated = computed(() => store.state.isAuthenticated)
|
||||||
|
|
||||||
|
/** Whether the user has any snoozed requests */
|
||||||
|
const hasSnoozed = computed(() =>
|
||||||
|
store.state.isAuthenticated &&
|
||||||
|
Array.isArray(store.state.journal) &&
|
||||||
|
store.state.journal.filter(req => req.snoozedUntil > Date.now()).length > 0)
|
||||||
|
|
||||||
|
/** Log a user on using Auth0 */
|
||||||
|
const logOn = () => auth.login()
|
||||||
|
|
||||||
|
/** Log a user off using Auth0 */
|
||||||
|
const logOff = () => {
|
||||||
|
auth.logout(store)
|
||||||
|
router.push("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Open a new window/tab with help */
|
||||||
|
const showHelp = () => { window.open("https://docs.prayerjournal.me", "_blank") }
|
||||||
|
</script>
|
|
@ -1,25 +1,93 @@
|
||||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
import {
|
||||||
import Home from '../views/Home.vue'
|
createRouter,
|
||||||
|
createWebHistory,
|
||||||
|
NavigationGuardNext,
|
||||||
|
RouteLocationNormalized,
|
||||||
|
RouteRecordRaw
|
||||||
|
} from "vue-router"
|
||||||
|
|
||||||
|
import auth from "@/auth"
|
||||||
|
import store, { Mutations } from "@/store"
|
||||||
|
import Home from "@/views/Home.vue"
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: "/",
|
||||||
name: 'Home',
|
name: "Home",
|
||||||
component: Home
|
component: Home,
|
||||||
|
meta: { title: "Welcome!" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: "/journal",
|
||||||
name: 'About',
|
name: "Journal",
|
||||||
// route level code-splitting
|
component: () => import(/* webpackChunkName: "journal" */ "@/views/Journal.vue"),
|
||||||
// this generates a separate chunk (about.[hash].js) for this route
|
meta: { auth: true, title: "Loading Prayer Journal..." }
|
||||||
// which is lazy-loaded when the route is visited.
|
},
|
||||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
{
|
||||||
|
path: "/legal/privacy-policy",
|
||||||
|
name: "PrivacyPolicy",
|
||||||
|
component: () => import(/* webpackChunkName: "legal" */ "@/views/legal/PrivacyPolicy.vue"),
|
||||||
|
meta: { title: "Privacy Policy" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/legal/terms-of-service",
|
||||||
|
name: "TermsOfService",
|
||||||
|
component: () => import(/* webpackChunkName: "legal" */ "@/views/legal/TermsOfService.vue"),
|
||||||
|
meta: { title: "Terms of Service" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/request/:id/edit",
|
||||||
|
name: "EditRequest",
|
||||||
|
component: () => import(/* webpackChunkName: "edit" */ "@/views/request/EditRequest.vue"),
|
||||||
|
meta: { auth: true, title: "Edit Prayer Request" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/request/:id/full",
|
||||||
|
name: "FullRequest",
|
||||||
|
component: () => import(/* webpackChunkName: "full" */ "@/views/request/FullRequest.vue"),
|
||||||
|
meta: { auth: true, title: "View Full Prayer Request" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/requests/active",
|
||||||
|
name: "ActiveRequests",
|
||||||
|
component: () => import(/* webpackChunkName: "list" */ "@/views/request/ActiveRequests.vue"),
|
||||||
|
meta: { auth: true, title: "All Active Requests" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/requests/answered",
|
||||||
|
name: "AnsweredRequests",
|
||||||
|
component: () => import(/* webpackChunkName: "list" */ "@/views/request/AnsweredRequests.vue"),
|
||||||
|
meta: { auth: true, title: "Answered Requests" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/requests/snoozed",
|
||||||
|
name: "SnoozedRequests",
|
||||||
|
component: () => import(/* webpackChunkName: "list" */ "@/views/request/SnoozedRequests.vue"),
|
||||||
|
meta: { auth: true, title: "Snoozed Requests" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/user/log-on",
|
||||||
|
name: "LogOn",
|
||||||
|
component: () => import(/* webpackChunkName: "logon" */ "@/views/user/LogOn.vue"),
|
||||||
|
meta: { title: "Logging On..." }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(process.env.BASE_URL),
|
history: createWebHistory(process.env.BASE_URL),
|
||||||
|
scrollBehavior: (to : RouteLocationNormalized, from : RouteLocationNormalized, savedPosition : any) => {
|
||||||
|
return savedPosition ?? { x: 0, y: 0 }
|
||||||
|
},
|
||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to : RouteLocationNormalized, from : RouteLocationNormalized, next : NavigationGuardNext) => {
|
||||||
|
// Check for routes requiring authentication
|
||||||
|
if (!store.state.isAuthenticated && (to.meta.auth || false)) {
|
||||||
|
return auth.login({ target: to.path })
|
||||||
|
}
|
||||||
|
store.commit(Mutations.SetTitle, to.meta.title ?? "")
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
17
src/MyPrayerJournal/App/src/store/actions.ts
Normal file
17
src/MyPrayerJournal/App/src/store/actions.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/** Action to add a prayer request (pass request text) */
|
||||||
|
export const AddRequest = "add-request"
|
||||||
|
|
||||||
|
/** Action to check if a user is authenticated, refreshing the session first if it exists */
|
||||||
|
export const CheckAuthentication = "check-authentication"
|
||||||
|
|
||||||
|
/** Action to load the user's prayer journal */
|
||||||
|
export const LoadJournal = "load-journal"
|
||||||
|
|
||||||
|
/** Action to update a request */
|
||||||
|
export const UpdateRequest = "update-request"
|
||||||
|
|
||||||
|
/** Action to skip the remaining recurrence period */
|
||||||
|
export const ShowRequestNow = "show-request-now"
|
||||||
|
|
||||||
|
/** Action to snooze a request */
|
||||||
|
export const SnoozeRequest = "snooze-request"
|
|
@ -1,12 +1,194 @@
|
||||||
import { createStore } from 'vuex'
|
import { InjectionKey } from "vue"
|
||||||
|
import { createStore, Store, useStore as baseUseStore } from "vuex"
|
||||||
|
import { useTitle } from "@vueuse/core"
|
||||||
|
|
||||||
|
import api, { JournalRequest } from "@/api"
|
||||||
|
import auth from "@/auth"
|
||||||
|
|
||||||
|
import * as Actions from "./actions"
|
||||||
|
import * as Mutations from "./mutations"
|
||||||
|
|
||||||
|
/** The state of myPrayerJournal */
|
||||||
|
export interface State {
|
||||||
|
pageTitle : string,
|
||||||
|
/** The user's profile */
|
||||||
|
user : any,
|
||||||
|
/** Whether there is a user signed in */
|
||||||
|
isAuthenticated : boolean,
|
||||||
|
/** The current set of prayer requests */
|
||||||
|
journal : JournalRequest[],
|
||||||
|
/** Whether the journal is currently being loaded */
|
||||||
|
isLoadingJournal : boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An injection key to identify this state with Vue */
|
||||||
|
export const key : InjectionKey<Store<State>> = Symbol("VueX Store")
|
||||||
|
|
||||||
|
/** Use this store in component `setup` functions */
|
||||||
|
export function useStore () : Store<State> {
|
||||||
|
return baseUseStore(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the "Bearer" authorization header with the current access token
|
||||||
|
*/
|
||||||
|
const setBearer = async function () {
|
||||||
|
try {
|
||||||
|
await auth.getAccessToken()
|
||||||
|
api.setBearer(auth.session.id.token)
|
||||||
|
} catch (err : any) {
|
||||||
|
if (err.message === "Not logged in") {
|
||||||
|
console.warn("API request attempted when user was not logged in")
|
||||||
|
} else {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The name of the application */
|
||||||
|
const appName = "myPrayerJournal"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the sort value for a prayer request
|
||||||
|
* @param it The prayer request
|
||||||
|
*/
|
||||||
|
const sortValue = (it : JournalRequest) => it.showAfter === 0 ? it.asOf : it.showAfter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort journal requests either by asOf or showAfter
|
||||||
|
*/
|
||||||
|
const journalSort = (a : JournalRequest, b : JournalRequest) => sortValue(a) - sortValue(b)
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
state: {
|
state: () : State => ({
|
||||||
},
|
pageTitle: appName,
|
||||||
|
user: auth.session.profile,
|
||||||
|
isAuthenticated: auth.isAuthenticated(),
|
||||||
|
journal: [],
|
||||||
|
isLoadingJournal: false
|
||||||
|
}),
|
||||||
mutations: {
|
mutations: {
|
||||||
|
[Mutations.LoadingJournal] (state, flag : boolean) {
|
||||||
|
state.isLoadingJournal = flag
|
||||||
|
},
|
||||||
|
[Mutations.LoadedJournal] (state, journal : JournalRequest[]) {
|
||||||
|
state.journal = journal.sort(journalSort)
|
||||||
|
},
|
||||||
|
[Mutations.RequestAdded] (state, newRequest : JournalRequest) {
|
||||||
|
state.journal.push(newRequest)
|
||||||
|
},
|
||||||
|
[Mutations.RequestUpdated] (state, request : JournalRequest) {
|
||||||
|
const jrnl = state.journal.filter(it => it.requestId !== request.requestId)
|
||||||
|
if (request.lastStatus !== "Answered") jrnl.push(request)
|
||||||
|
state.journal = jrnl
|
||||||
|
},
|
||||||
|
[Mutations.SetAuthentication] (state, value : boolean) {
|
||||||
|
state.isAuthenticated = value
|
||||||
|
},
|
||||||
|
[Mutations.SetTitle]: (state, title : string) => {
|
||||||
|
state.pageTitle = title === "" ? appName : `${title} | ${appName}`
|
||||||
|
useTitle(state.pageTitle)
|
||||||
|
},
|
||||||
|
[Mutations.UserLoggedOff] (state) {
|
||||||
|
state.user = {}
|
||||||
|
api.removeBearer()
|
||||||
|
state.isAuthenticated = false
|
||||||
|
},
|
||||||
|
[Mutations.UserLoggedOn] (state, user) {
|
||||||
|
state.user = user
|
||||||
|
state.isAuthenticated = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
},
|
// async [Actions.AddRequest] ({ commit }, p : AddRequestAction) {
|
||||||
modules: {
|
// const { progress, requestText, recurType, recurCount } = p
|
||||||
|
// progress.events.$emit('show', 'indeterminate')
|
||||||
|
// try {
|
||||||
|
// await setBearer()
|
||||||
|
// const newRequest = await api.addRequest(requestText, recurType, recurCount)
|
||||||
|
// commit(Mutations.RequestAdded, newRequest.data)
|
||||||
|
// } catch (err) {
|
||||||
|
// logError(err)
|
||||||
|
// } finally {
|
||||||
|
// progress.events.$emit("done")
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
async [Actions.CheckAuthentication] ({ commit }) {
|
||||||
|
try {
|
||||||
|
await auth.getAccessToken()
|
||||||
|
commit(Mutations.SetAuthentication, auth.isAuthenticated())
|
||||||
|
} catch (_) {
|
||||||
|
commit(Mutations.SetAuthentication, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ,
|
||||||
|
// async [Actions.LoadJournal] ({ commit }, progress : ProgressProps) {
|
||||||
|
// commit(Mutations.LoadedJournal, [])
|
||||||
|
// progress.events.$emit("show", "query")
|
||||||
|
// commit(Mutations.LoadingJournal, true)
|
||||||
|
// await setBearer()
|
||||||
|
// try {
|
||||||
|
// const jrnl = await api.journal()
|
||||||
|
// commit(Mutations.LoadedJournal, jrnl.data)
|
||||||
|
// } catch (err) {
|
||||||
|
// logError(err)
|
||||||
|
// } finally {
|
||||||
|
// progress.events.$emit("done")
|
||||||
|
// commit(Mutations.LoadingJournal, false)
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// async [Actions.UpdateRequest] ({ commit, state }, p : UpdateRequestAction) {
|
||||||
|
// const { progress, requestId, status, updateText, recurType, recurCount } = p
|
||||||
|
// progress.events.$emit("show", "indeterminate")
|
||||||
|
// try {
|
||||||
|
// await setBearer()
|
||||||
|
// const oldReq = (state.journal.filter(req => req.requestId === requestId) || [])[0] || {}
|
||||||
|
// if (!(status === "Prayed" && updateText === "")) {
|
||||||
|
// if (status !== "Answered" && (oldReq.recurType !== recurType || oldReq.recurCount !== recurCount)) {
|
||||||
|
// await api.updateRecurrence(requestId, recurType, recurCount)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (status !== "Updated" || oldReq.text !== updateText) {
|
||||||
|
// await api.updateRequest(requestId, status, oldReq.text !== updateText ? updateText : "")
|
||||||
|
// }
|
||||||
|
// const request = await api.getRequest(requestId)
|
||||||
|
// commit(Mutations.RequestUpdated, request.data)
|
||||||
|
// } catch (err) {
|
||||||
|
// logError(err)
|
||||||
|
// } finally {
|
||||||
|
// progress.events.$emit('done')
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// async [Actions.ShowRequestNow] ({ commit }, p : ShowRequestAction) {
|
||||||
|
// const { progress, requestId, showAfter } = p
|
||||||
|
// progress.events.$emit("show", "indeterminate")
|
||||||
|
// try {
|
||||||
|
// await setBearer()
|
||||||
|
// await api.showRequest(requestId, showAfter)
|
||||||
|
// const request = await api.getRequest(requestId)
|
||||||
|
// commit(Mutations.RequestUpdated, request.data)
|
||||||
|
// } catch (err) {
|
||||||
|
// logError(err)
|
||||||
|
// } finally {
|
||||||
|
// progress.events.$emit('done')
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// async [Actions.SnoozeRequest] ({ commit }, p : SnoozeRequestAction) {
|
||||||
|
// const { progress, requestId, until } = p
|
||||||
|
// progress.events.$emit("show", "indeterminate")
|
||||||
|
// try {
|
||||||
|
// await setBearer()
|
||||||
|
// await api.snoozeRequest(requestId, until)
|
||||||
|
// const request = await api.getRequest(requestId)
|
||||||
|
// commit(Mutations.RequestUpdated, request.data)
|
||||||
|
// } catch (err) {
|
||||||
|
// logError(err)
|
||||||
|
// } finally {
|
||||||
|
// progress.events.$emit("done")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export * as Actions from "./actions"
|
||||||
|
export * as Mutations from "./mutations"
|
||||||
|
|
23
src/MyPrayerJournal/App/src/store/mutations.ts
Normal file
23
src/MyPrayerJournal/App/src/store/mutations.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/** Mutation for when the user's prayer journal is being loaded */
|
||||||
|
export const LoadingJournal = "loading-journal"
|
||||||
|
|
||||||
|
/** Mutation for when the user's prayer journal has been loaded */
|
||||||
|
export const LoadedJournal = "journal-loaded"
|
||||||
|
|
||||||
|
/** Mutation for adding a new prayer request (pass text) */
|
||||||
|
export const RequestAdded = "request-added"
|
||||||
|
|
||||||
|
/** Mutation to replace a prayer request at the top of the current journal */
|
||||||
|
export const RequestUpdated = "request-updated"
|
||||||
|
|
||||||
|
/** Mutation for setting the authentication state */
|
||||||
|
export const SetAuthentication = "set-authentication"
|
||||||
|
|
||||||
|
/** Mutation for setting the page title */
|
||||||
|
export const SetTitle = "set-title"
|
||||||
|
|
||||||
|
/** Mutation for logging a user off */
|
||||||
|
export const UserLoggedOff = "user-logged-off"
|
||||||
|
|
||||||
|
/** Mutation for logging a user on (pass user) */
|
||||||
|
export const UserLoggedOn = "user-logged-on"
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,18 +1,12 @@
|
||||||
<template>
|
<template lang="pug">
|
||||||
<div class="home">
|
main.container
|
||||||
<img alt="Vue logo" src="../assets/logo.png">
|
p
|
||||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
|
p.
|
||||||
</div>
|
myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them,
|
||||||
|
update them as God moves in the situation, and record a final answer received on that request. It also allows
|
||||||
|
individuals to review their answered prayers.
|
||||||
|
p.
|
||||||
|
This site is open and available to the general public. To get started, simply click the “Log On” link
|
||||||
|
above, and log on with either a Microsoft or Google account. You can also learn more about the site at the
|
||||||
|
“Docs” link, also above.
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Options, Vue } from 'vue-class-component';
|
|
||||||
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
|
|
||||||
|
|
||||||
@Options({
|
|
||||||
components: {
|
|
||||||
HelloWorld,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Home extends Vue {}
|
|
||||||
</script>
|
|
||||||
|
|
57
src/MyPrayerJournal/App/src/views/legal/PrivacyPolicy.vue
Normal file
57
src/MyPrayerJournal/App/src/views/legal/PrivacyPolicy.vue
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<template lang="pug">
|
||||||
|
md-content(role='main').mpj-main-content
|
||||||
|
md-card
|
||||||
|
md-card-header
|
||||||
|
.md-title Privacy Policy
|
||||||
|
.md-subhead as of May 21, 2018
|
||||||
|
md-card-content.mpj-full-page-card
|
||||||
|
p.
|
||||||
|
The nature of the service is one where privacy is a must. The items below will help you understand the data we
|
||||||
|
collect, access, and store on your behalf as you use this service.
|
||||||
|
hr
|
||||||
|
h3 Third Party Services
|
||||||
|
p.
|
||||||
|
myPrayerJournal utilizes a third-party authentication and identity provider. You should familiarize yourself
|
||||||
|
with the privacy policy for #[a(href='https://auth0.com/privacy' target='_blank') Auth0], as well as your
|
||||||
|
chosen provider (#[a(href='https://privacy.microsoft.com/en-us/privacystatement' target='_blank') Microsoft] or
|
||||||
|
#[a(href='https://policies.google.com/privacy' target='_blank') Google]).
|
||||||
|
hr
|
||||||
|
h3 What We Collect
|
||||||
|
h4 Identifying Data
|
||||||
|
ul
|
||||||
|
li.
|
||||||
|
The only identifying data myPrayerJournal stores is the subscriber (“sub”) field from the token we
|
||||||
|
receive from Auth0, once you have signed in through their hosted service. All information is associated with
|
||||||
|
you via this field.
|
||||||
|
li.
|
||||||
|
While you are signed in, within your browser, the service has access to your first and last names, along with
|
||||||
|
a URL to the profile picture (provided by your selected identity provider). This information is not
|
||||||
|
transmitted to the server, and is removed when “Log Off” is clicked.
|
||||||
|
h4 User Provided Data
|
||||||
|
ul
|
||||||
|
li.
|
||||||
|
myPrayerJournal stores the information you provide, including the text of prayer requests, updates, and notes;
|
||||||
|
and the date/time when certain actions are taken.
|
||||||
|
hr
|
||||||
|
h3 How Your Data Is Accessed / Secured
|
||||||
|
ul
|
||||||
|
li.
|
||||||
|
Your provided data is returned to you, as required, to display your journal or your answered requests. On the
|
||||||
|
server, it is stored in a controlled-access database.
|
||||||
|
li.
|
||||||
|
Your data is backed up, along with other Bit Badger Solutions hosted systems, in a rolling manner; backups are
|
||||||
|
preserved for the prior 7 days, and backups from the 1st and 15th are preserved for 3 months. These backups
|
||||||
|
are stored in a private cloud data repository.
|
||||||
|
li.
|
||||||
|
The data collected and stored is the absolute minimum necessary for the functionality of the service. There
|
||||||
|
are no plans to “monetize” this service, and storing the minimum amount of information means that
|
||||||
|
the data we have is not interesting to purchasers (or those who may have more nefarious purposes).
|
||||||
|
li Access to servers and backups is strictly controlled and monitored for unauthorized access attempts.
|
||||||
|
hr
|
||||||
|
h3 Removing Your Data
|
||||||
|
p.
|
||||||
|
At any time, you may choose to discontinue using this service. Both Microsoft and Google provide ways to revoke
|
||||||
|
access from this application. However, if you want your data removed from the database, please contact daniel at
|
||||||
|
bitbadger.solutions (via e-mail, replacing at with @) prior to doing so, to ensure we can determine which
|
||||||
|
subscriber ID belongs to you.
|
||||||
|
</template>
|
38
src/MyPrayerJournal/App/src/views/legal/TermsOfService.vue
Normal file
38
src/MyPrayerJournal/App/src/views/legal/TermsOfService.vue
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<template lang="pug">
|
||||||
|
md-content(role='main').mpj-main-content
|
||||||
|
md-card
|
||||||
|
md-card-header
|
||||||
|
.md-title Terms of Service
|
||||||
|
.md-subhead as of May 21, 2018
|
||||||
|
md-card-content.mpj-full-page-card
|
||||||
|
h3 1. Acceptance of Terms
|
||||||
|
p.
|
||||||
|
By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you are
|
||||||
|
responsible to ensure that your use of this site complies with all applicable laws. Your continued use of this
|
||||||
|
site implies your acceptance of these terms.
|
||||||
|
h3 2. Description of Service and Registration
|
||||||
|
p.
|
||||||
|
myPrayerJournal is a service that allows individuals to enter and amend their prayer requests. It requires no
|
||||||
|
registration by itself, but access is granted based on a successful login with an external identity provider.
|
||||||
|
See #[router-link(:to="{ name: 'PrivacyPolicy' }") our privacy policy] for details on how that information is
|
||||||
|
accessed and stored.
|
||||||
|
h3 3. Third Party Services
|
||||||
|
p.
|
||||||
|
This service utilizes a third-party service provider for identity management. Review the terms of service for
|
||||||
|
#[a(href='https://auth0.com/terms' target='_blank') Auth0], as well as those for the selected authorization
|
||||||
|
provider (#[a(href='https://www.microsoft.com/en-us/servicesagreement' target='_blank') Microsoft] or
|
||||||
|
#[a(href='https://policies.google.com/terms' target='_blank') Google]).
|
||||||
|
h3 4. Liability
|
||||||
|
p.
|
||||||
|
This service is provided "as is", and no warranty (express or implied) exists. The service and its developers
|
||||||
|
may not be held liable for any damages that may arise through the use of this service.
|
||||||
|
h3 5. Updates to Terms
|
||||||
|
p.
|
||||||
|
These terms and conditions may be updated at any time, and this service does not have the capability to notify
|
||||||
|
users when these change. The date at the top of the page will be updated when any of the text of these terms is
|
||||||
|
updated.
|
||||||
|
hr
|
||||||
|
p.
|
||||||
|
You may also wish to review our #[router-link(:to="{ name: 'PrivacyPolicy' }") privacy policy] to learn how we
|
||||||
|
handle your data.
|
||||||
|
</template>
|
27
src/MyPrayerJournal/App/src/views/user/LogOn.vue
Normal file
27
src/MyPrayerJournal/App/src/views/user/LogOn.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<template lang="pug">
|
||||||
|
main.container
|
||||||
|
p Logging you on...
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, onBeforeMount } from "vue"
|
||||||
|
import { useRouter } from "vue-router"
|
||||||
|
|
||||||
|
import { AuthService } from "@/auth"
|
||||||
|
import { useStore } from "@/store"
|
||||||
|
import { AuthSymbol } from "@/App.vue"
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
/** Auth service instance */
|
||||||
|
const auth = inject(AuthSymbol) as AuthService
|
||||||
|
|
||||||
|
/** Navigate on auth completion */
|
||||||
|
auth.on("loginEvent", (data: any) => {
|
||||||
|
router.push(data.state.target ?? "/journal")
|
||||||
|
})
|
||||||
|
|
||||||
|
// this.progress.$emit('show', 'indeterminate')
|
||||||
|
onBeforeMount(async () => { await auth.handleAuthentication(store) })
|
||||||
|
</script>
|
13
vetur.config.js
Normal file
13
vetur.config.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// vetur.config.js
|
||||||
|
/** @type {import('vls').VeturConfig} */
|
||||||
|
module.exports = {
|
||||||
|
// override vscode settings
|
||||||
|
// Notice: It only affects the settings used by Vetur.
|
||||||
|
settings: {
|
||||||
|
// "vetur.useWorkspaceDependencies": true,
|
||||||
|
// "vetur.experimental.templateInterpolationService": true
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
'./src/MyPrayerJournal/App'
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user