WIP on login from NAS

This commit is contained in:
Daniel J. Summers 2021-07-21 23:04:11 -04:00
parent 3927a4cb22
commit 4fce076ee5
10 changed files with 141 additions and 49 deletions

View File

@ -30,7 +30,7 @@ export default defineComponent({
AppNav, AppNav,
TitleBar TitleBar
}, },
data () { setup () {
return { return {
// //
} }

View File

@ -0,0 +1,25 @@
import { LogOnSuccess } 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}`
export default {
/**
* Log a citizen on
* @param code The authorization code from No Agenda Social
* @returns The user result, or an error
*/
logOn: async (code : string) : Promise<LogOnSuccess | string> => {
const resp = await fetch(apiUrl(`/citizen/log-on/${code}`), { method: 'GET' })
if (resp.status === 200) return await resp.json() as LogOnSuccess
console.error(await resp.text())
return 'Error logging on'
}
}
export * from './types'

View File

@ -0,0 +1,10 @@
/** A successful logon */
export interface LogOnSuccess {
/** The JSON Web Token (JWT) to use for API access */
jwt : string
/** The ID of the logged-in citizen (as a string) */
citizenId : string
/** The name of the logged-in citizen */
name : string
}

View File

@ -4,20 +4,6 @@
</p> </p>
</template> </template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'AppFooter',
data () {
return {
//
}
}
})
</script>
<style lang="sass" scoped> <style lang="sass" scoped>
p p
padding-top: 2rem padding-top: 2rem

View File

@ -6,7 +6,7 @@
<router-link to="/"><v-icon icon="mdi-home" /> Home</router-link> <router-link to="/"><v-icon icon="mdi-home" /> Home</router-link>
<!-- If not logged in --> <!-- If not logged in -->
<router-link to="/profile/seeking"><v-icon icon="mdi-view-list-outline" /> Job Seekers</router-link> <router-link to="/profile/seeking"><v-icon icon="mdi-view-list-outline" /> Job Seekers</router-link>
<router-link to="/log-on"><v-icon icon="mdi-login-variant" /> Log On</router-link> <a :href="authUrl"><v-icon icon="mdi-login-variant" /> Log On</a>
<!-- If logged in --> <!-- If logged in -->
<router-link to="/citizen/profile"><v-icon icon="mdi-pencil" /> Edit Your Profile</router-link> <router-link to="/citizen/profile"><v-icon icon="mdi-pencil" /> Edit Your Profile</router-link>
<router-link to="/profile/search"><v-icon icon="mdi-view-list-outline" /> View Profiles</router-link> <router-link to="/profile/search"><v-icon icon="mdi-view-list-outline" /> View Profiles</router-link>
@ -23,10 +23,19 @@ import { defineComponent } from 'vue'
export default defineComponent({ export default defineComponent({
name: 'AppNav', name: 'AppNav',
setup () {
data () {
return { return {
// /** The authorization URL to which the user should be directed */
authUrl: (() => {
/** The client ID for Jobs, Jobs, Jobs at No Agenda Social */
const id = 'k_06zlMy0N451meL4AqlwMQzs5PYr6g3d2Q_dCT-OjU'
const client = `client_id=${id}`
const scope = 'scope=read:accounts'
const redirect = `redirect_uri=${document.location.origin}/citizen/authorized`
const respType = 'response_type=code'
// TODO: move NAS base URL to config
return `https://noagendasocial.com/oauth/authorize?${client}&${scope}&${redirect}&${respType}`
})()
} }
} }
}) })

View File

@ -13,16 +13,20 @@ export default defineComponent({
required: true required: true
} }
}, },
computed: { setup (props) {
clipSource () : string { /** The full relative URL for the audio clip */
return `/audio/${this.clip}.mp3` const clipSource = `/audio/${props.clip}.mp3`
}
}, /** Play the audio file */
methods: { const playFile = () => {
playFile () { const audio = document.getElementById(props.clip) as HTMLAudioElement
const audio = document.getElementById(this.clip) as HTMLAudioElement
audio.play() audio.play()
} }
return {
clipSource,
playFile
}
} }
}) })
</script> </script>

View File

@ -2,10 +2,10 @@ import { createApp } from 'vue'
import vuetify from './plugins/vuetify' import vuetify from './plugins/vuetify'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import store from './store' import store, { key } from './store'
createApp(App) createApp(App)
.use(router) .use(router)
.use(store) .use(store, key)
.use(vuetify) .use(vuetify)
.mount('#app') .mount('#app')

View File

@ -2,30 +2,20 @@ import { createRouter, createWebHistory, RouteLocationNormalized, RouteLocationN
import Home from '../views/Home.vue' import Home from '../views/Home.vue'
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ path: '/', component: Home },
{ path: '/how-it-works', component: () => import(/* webpackChunkName: "help" */ '../views/HowItWorks.vue') },
{ path: '/privacy-policy', component: () => import(/* webpackChunkName: "legal" */ '../views/PrivacyPolicy.vue') },
{ path: '/terms-of-service', component: () => import(/* webpackChunkName: "legal" */ '../views/TermsOfService.vue') },
// Citizen URLs
{ {
path: '/', path: '/citizen/authorized',
name: 'Home', component: () => import(/* webpackChunkName: "logon" */ '../views/citizen/Authorized.vue')
component: Home
},
{
path: '/how-it-works',
name: 'HowItWorks',
component: () => import(/* webpackChunkName: "help" */ '../views/HowItWorks.vue')
},
{
path: '/privacy-policy',
name: 'PrivacyPolicy',
component: () => import(/* webpackChunkName: "privacy" */ '../views/PrivacyPolicy.vue')
},
{
path: '/terms-of-service',
name: 'TermsOfService',
component: () => import(/* webpackChunkName: "terms" */ '../views/TermsOfService.vue')
} }
] ]
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(process.env.BASE_URL),
// eslint-disable-next-line
scrollBehavior (to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, savedPosition: any) { scrollBehavior (to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, savedPosition: any) {
return savedPosition ?? { top: 0, left: 0 } return savedPosition ?? { top: 0, left: 0 }
}, },

View File

@ -1,11 +1,45 @@
import { createStore } from 'vuex' import { InjectionKey } from 'vue'
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import api, { LogOnSuccess } from '../api'
/** The state tracked by the application */
export interface State {
user: LogOnSuccess | undefined
logOnState: string
}
/** 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)
}
export default createStore({ export default createStore({
state: { state: () : State => {
return {
user: undefined,
logOnState: 'Logging you on with No Agenda Social...'
}
}, },
mutations: { mutations: {
setUser (state, user: LogOnSuccess) {
state.user = user
},
setLogOnState (state, message) {
state.logOnState = message
}
}, },
actions: { actions: {
async logOn ({ commit }, code: string) {
const logOnResult = await api.logOn(code)
if (typeof logOnResult === 'string') {
commit('setLogOnState', logOnResult)
} else {
commit('setUser', logOnResult)
}
}
}, },
modules: { modules: {
} }

View File

@ -0,0 +1,34 @@
<template>
<p>{{message}}</p>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from '../../store'
export default defineComponent({
name: 'Authorized',
setup () {
const router = useRouter()
const store = useStore()
/** Pass the code to the API and exchange it for a user and a JWT */
const logOn = async () => {
const code = router.currentRoute.value.query.code
if (code) {
await store.dispatch('logOn', code)
if (store.state.user !== undefined) { router.push('/citizen/dashboard') }
} else {
store.commit('setLogOnState', 'Did not receive a token from No Agenda Social (perhaps you clicked "Cancel"?)')
}
}
onMounted(logOn)
return {
message: computed(() => store.state.logOnState)
}
}
})
</script>