WIP on multi-instance login (#22)

Something isn't lining up between the client and server (need to debug the URLs they're both using)
This commit is contained in:
2021-09-01 22:23:18 -04:00
parent 45861e06f0
commit 4e84bc251a
12 changed files with 291 additions and 111 deletions

View File

@@ -2,6 +2,7 @@ import {
Citizen,
Continent,
Count,
Instance,
Listing,
ListingExpireForm,
ListingForm,
@@ -100,11 +101,12 @@ export default {
/**
* Log a citizen on
*
* @param code The authorization code from No Agenda Social
* @param abbr The abbreviation of the Mastodon instance that issued the code
* @param code The authorization code from Mastodon
* @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", mode: "cors" })
logOn: async (abbr : string, code : string) : Promise<LogOnSuccess | string> => {
const resp = await fetch(apiUrl(`citizen/log-on/${abbr}/${code}`), { method: "GET", mode: "cors" })
if (resp.status === 200) return await resp.json() as LogOnSuccess
return `Error logging on - ${await resp.text()}`
},
@@ -141,6 +143,27 @@ export default {
apiResult<Continent[]>(await fetch(apiUrl("continents"), { method: "GET" }), "retrieving continents")
},
/** API functions for instances */
instances: {
/**
* Get all Mastodon instances we support
*
* @returns All instances, or an error
*/
all: async () : Promise<Instance[] | string | undefined> =>
apiResult<Instance[]>(await fetch(apiUrl("instances"), { method: "GET" }), "retrieving Mastodon instances"),
/**
* Retrieve a Mastodon instance by its abbreviation
*
* @param abbr The abbreviation of the Mastodon instance to retrieve
* @returns The Mastodon instance (if found), undefined (if not found), or an error string
*/
byAbbr: async (abbr : string) : Promise<Instance | string | undefined> =>
apiResult<Instance>(await fetch(apiUrl(`instance/${abbr}`), { method: "GET" }), "retrieving Mastodon instance")
},
/** API functions for job listings */
listings: {

View File

@@ -31,6 +31,18 @@ export interface Count {
count : number
}
/** The Mastodon instance data provided via the Jobs, Jobs, Jobs API */
export interface Instance {
/** The name of the instance */
name : string
/** The URL for this instance */
url : string
/** The abbreviation used in the URL to distinguish this instance's return codes */
abbr : string
/** The client ID (assigned by the Mastodon server) */
clientId : string
}
/** A job listing */
export interface Listing {
/** The ID of the job listing */

View File

@@ -53,7 +53,7 @@ const routes: Array<RouteRecordRaw> = [
component: LogOn
},
{
path: "/citizen/authorized",
path: "/citizen/:abbr/authorized",
name: "CitizenAuthorized",
component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Authorized.vue")
},

View File

@@ -43,8 +43,8 @@ export default createStore({
}
},
actions: {
async logOn ({ commit }, code: string) {
const logOnResult = await api.citizen.logOn(code)
async logOn ({ commit }, { abbr, code }) {
const logOnResult = await api.citizen.logOn(abbr, code)
if (typeof logOnResult === "string") {
commit("setLogOnState", logOnResult)
} else {

View File

@@ -7,30 +7,44 @@ article
<script setup lang="ts">
import { computed, onMounted } from "vue"
import { useRouter } from "vue-router"
import { useRoute, useRouter } from "vue-router"
import api from "@/api"
import { useStore } from "@/store"
import { AFTER_LOG_ON_URL } from "@/router"
const router = useRouter()
const store = useStore()
const route = useRoute()
const router = useRouter()
/** The abbreviation of the instance from which we received the code */
const abbr = route.params.abbr as string
/** Set the message for this component */
const setMessage = (msg : string) => store.commit("setLogOnState", msg)
/** 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) {
const afterLogOnUrl = window.localStorage.getItem(AFTER_LOG_ON_URL)
if (afterLogOnUrl) {
window.localStorage.removeItem(AFTER_LOG_ON_URL)
router.push(afterLogOnUrl)
} else {
router.push("/citizen/dashboard")
}
}
const instance = await api.instances.byAbbr(abbr)
if (typeof instance === "string") {
setMessage(instance)
} else if (typeof instance === "undefined") {
setMessage(`Mastodon instance ${abbr} not found`)
} else {
store.commit("setLogOnState",
"Did not receive a token from No Agenda Social (perhaps you clicked &ldquo;Cancel&rdquo;?)")
const code = route.query.code
if (code) {
await store.dispatch("logOn", { abbr, code })
if (store.state.user !== undefined) {
const afterLogOnUrl = window.localStorage.getItem(AFTER_LOG_ON_URL)
if (afterLogOnUrl) {
window.localStorage.removeItem(AFTER_LOG_ON_URL)
router.push(afterLogOnUrl)
} else {
router.push("/citizen/dashboard")
}
}
} else {
setMessage(`Did not receive a token from ${instance.name} (perhaps you clicked &ldquo;Cancel&rdquo;?)`)
}
}
}

View File

@@ -1,24 +1,58 @@
<template lang="pug">
article
p &nbsp;
p.fst-italic Sending you over to No Agenda Social to log on; see you back in just a second&hellip;
load-data(:load="retrieveInstances")
p.fst-italic(v-if="selected") Sending you over to {{selected.name}} to log on; see you back in just a second&hellip;
template(v-else)
p.text-center Please select your No Agenda-affiliated Mastodon instance
p.text-center(v-for="it in instances" :key="it.abbr")
button.btn.btn-primary(@click.prevent="select(it.abbr)") {{it.name}}
</template>
<script setup lang="ts">
/**
* This component simply redirects the user to the No Agenda Social authorization page; it is separate here so that it
* can be called from two different places, and allow the app to support direct links to authorized content.
*/
import { computed, Ref, ref } from "vue"
import api, { Instance } from "@/api"
import LoadData from "@/components/LoadData.vue"
/** The instances configured for Jobs, Jobs, Jobs */
const instances : Ref<Instance[]> = ref([])
/** Whether authorization is in progress */
const selected : Ref<Instance | undefined> = ref(undefined)
/** The authorization URL to which the user should be directed */
const 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"
return `https://noagendasocial.com/oauth/authorize?${client}&${scope}&${redirect}&${respType}`
})()
document.location.assign(authUrl)
const authUrl = computed(() => {
if (selected.value) {
/** The client ID for Jobs, Jobs, Jobs at No Agenda Social */
const client = `client_id=${selected.value.clientId}`
const scope = "scope=read:accounts"
const redirect = `redirect_uri=${document.location.origin}/citizen/${selected.value.abbr}/authorized`
const respType = "response_type=code"
return `${selected.value.url}/oauth/authorize?${client}&${scope}&${redirect}&${respType}`
}
return ""
})
/**
* Select a given Mastadon instance
*
* @param abbr The abbreviation of the instance being selected
*/
const select = (abbr : string) => {
selected.value = instances.value.find(it => it.abbr === abbr)
document.location.assign(authUrl.value)
}
/** Load the instances we have configured */
const retrieveInstances = async (errors : string[]) => {
const instancesResp = await api.instances.all()
if (typeof instancesResp === "string") {
errors.push(instancesResp)
} else if (typeof instancesResp === "undefined") {
errors.push("No instances found (this should not happen)")
} else {
instances.value = instancesResp
}
}
</script>