Version 3 #67

Merged
danieljsummers merged 53 commits from version-3 into master 2021-10-26 23:39:59 +00:00
21 changed files with 2190 additions and 206 deletions
Showing only changes of commit b819b5e600 - Show all commits

File diff suppressed because it is too large Load Diff

View File

@ -13,13 +13,19 @@
"vue": "vue-cli-service build --modern && cd ../Api && dotnet run"
},
"dependencies": {
"@vueuse/core": "^6.4.1",
"auth0-js": "^9.16.4",
"bootstrap": "^5.1.1",
"core-js": "^3.6.5",
"date-fns": "^2.24.0",
"vue": "^3.0.0",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@types/auth0-js": "^9.14.5",
"@types/events": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "~4.5.0",
@ -34,6 +40,7 @@
"eslint-plugin-vue": "^7.0.0",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "~4.1.5"
"typescript": "~4.1.5",
"vue-cli-plugin-pug": "~2.0.0"
}
}

View File

@ -1,30 +1,38 @@
<template>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
<template lang="pug">
.mpj-app
#nav
router-link(to="/") Home
| |
router-link(to="/about") About
router-view
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
<script setup lang="ts">
import { provide } from "vue"
#nav {
padding: 30px;
import "bootstrap/dist/css/bootstrap.min.css"
a {
font-weight: bold;
color: #2c3e50;
import { AuthService } from "./auth"
&.router-link-exact-active {
color: #42b983;
}
}
}
/** The auth service injection symbol */
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>

View 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'

View 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[] = []
}

View 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'
}

View 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()

View 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)
}

View 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>

View File

@ -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>

View 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>

View File

@ -1,25 +1,93 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
import {
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> = [
{
path: '/',
name: 'Home',
component: Home
path: "/",
name: "Home",
component: Home,
meta: { title: "Welcome!" }
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
path: "/journal",
name: "Journal",
component: () => import(/* webpackChunkName: "journal" */ "@/views/Journal.vue"),
meta: { auth: true, title: "Loading Prayer Journal..." }
},
{
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({
history: createWebHistory(process.env.BASE_URL),
scrollBehavior: (to : RouteLocationNormalized, from : RouteLocationNormalized, savedPosition : any) => {
return savedPosition ?? { x: 0, y: 0 }
},
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

View 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"

View File

@ -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({
state: {
},
state: () : State => ({
pageTitle: appName,
user: auth.session.profile,
isAuthenticated: auth.isAuthenticated(),
journal: [],
isLoadingJournal: false
}),
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: {
},
modules: {
// async [Actions.AddRequest] ({ commit }, p : AddRequestAction) {
// 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"

View 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"

View File

@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@ -1,18 +1,12 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
</div>
<template lang="pug">
main.container
p &nbsp;
p.
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 &ldquo;Log On&rdquo; link
above, and log on with either a Microsoft or Google account. You can also learn more about the site at the
&ldquo;Docs&rdquo; link, also above.
</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>

View 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 (&ldquo;sub&rdquo;) 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 &ldquo;Log Off&rdquo; 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 &ldquo;monetize&rdquo; 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>

View 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>

View 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
View 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'
]
}