Fix final issues with multi-instance (#22)

This commit is contained in:
Daniel J. Summers 2021-09-06 20:11:29 -04:00
parent 989beb0659
commit df6188dfd1
19 changed files with 126 additions and 116 deletions

View File

@ -360,10 +360,14 @@ module Citizen =
/// Find a citizen by their Mastodon username
let findByMastodonUser (instance : string) (mastodonUser : string) conn =
r.Table(Table.Citizen)
.GetAll(r.Array (instance, mastodonUser)).OptArg("index", "instanceUser").Nth(0)
.RunResultAsync<Citizen>
|> withReconnOption conn
fun c -> task {
let! u =
r.Table(Table.Citizen)
.GetAll(r.Array (instance, mastodonUser)).OptArg("index", "instanceUser").Limit(1)
.RunResultAsync<Citizen list> c
return u |> List.tryHead
}
|> withReconn conn
/// Add a citizen
let add (citizen : Citizen) conn =

View File

@ -201,14 +201,6 @@ module Instances =
fun next ctx -> task {
return! json ((authConfig ctx).Instances |> Array.map toInstance) next ctx
}
// GET: /api/instance/[abbr]
let byAbbr abbr : HttpHandler =
fun next ctx -> task {
match (authConfig ctx).Instances |> Array.tryFind (fun it -> it.Abbr = abbr) with
| Some inst -> return! json (toInstance inst) next ctx
| None -> return! Error.notFound next ctx
}
/// Handlers for /api/listing[s] routes
@ -530,12 +522,7 @@ let allEndpoints = [
DELETE [ route "" Citizen.delete ]
]
GET_HEAD [ route "/continents" Continent.all ]
subRoute "/instance" [
GET_HEAD [
route "s" Instances.all
routef "/%s" Instances.byAbbr
]
]
GET_HEAD [ route "/instances" Instances.all ]
subRoute "/listing" [
GET_HEAD [
routef "/%O" Listing.get

View File

@ -1,12 +1,13 @@
{
"name": "jobs-jobs-jobs",
"version": "2.0.0",
"version": "2.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"apiserve": "vue-cli-service build && cd ../Api && dotnet run -c Debug"
"apiserve": "vue-cli-service build && cd ../Api && dotnet run -c Debug",
"publish": "vue-cli-service build --modern && cd ../Api && dotnet publish -c Release -r linux-x64 --self-contained false"
},
"dependencies": {
"@mdi/js": "^5.9.55",

View File

@ -40,7 +40,7 @@ export function yesOrNo (cond : boolean) : string {
}
/**
* Get the display name for a citizen (the first available among real, display, or NAS handle)
* Get the display name for a citizen (the first available among real, display, or Mastodon handle)
*
* @param cit The citizen
* @returns The citizen's display name

View File

@ -152,16 +152,7 @@ export default {
* @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")
apiResult<Instance[]>(await fetch(apiUrl("instances"), { method: "GET" }), "retrieving Mastodon instances")
},
/** API functions for job listings */

View File

@ -10,7 +10,7 @@ import store from "@/store"
import Home from "@/views/Home.vue"
import LogOn from "@/views/citizen/LogOn.vue"
/** The URL to which the user should be pointed once they have authorized with NAS */
/** The URL to which the user should be pointed once they have authorized with Mastodon */
export const AFTER_LOG_ON_URL = "jjj-after-log-on-url"
/**
@ -121,7 +121,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionOptions.vue")
},
{
path: "/so-long/success/:url",
path: "/so-long/success/:abbr",
name: "DeletionSuccess",
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionSuccess.vue")
},

View File

@ -0,0 +1,8 @@
/** Logs a user on to Jobs, Jobs, Jobs */
export const LogOn = "logOn"
/** Ensures that the continent list in the state has been populated */
export const EnsureContinents = "ensureContinents"
/** Ensures that the Mastodon instance list in the state has been populated */
export const EnsureInstances = "ensureInstances"

View File

@ -1,6 +1,8 @@
import { InjectionKey } from "vue"
import { createStore, Store, useStore as baseUseStore } from "vuex"
import api, { Continent, LogOnSuccess } from "../api"
import api, { Continent, Instance, LogOnSuccess } from "../api"
import * as Actions from "./actions"
import * as Mutations from "./mutations"
/** The state tracked by the application */
export interface State {
@ -10,6 +12,8 @@ export interface State {
logOnState: string
/** All continents (use `ensureContinents` action) */
continents: Continent[]
/** All instances (use `ensureInstances` action) */
instances: Instance[]
}
/** An injection key to identify this state with Vue */
@ -25,42 +29,50 @@ export default createStore({
return {
user: undefined,
logOnState: "<em>Welcome back!</em>",
continents: []
continents: [],
instances: []
}
},
mutations: {
setUser (state, user : LogOnSuccess) {
state.user = user
},
clearUser (state) {
state.user = undefined
},
setLogOnState (state, message : string) {
state.logOnState = message
},
setContinents (state, continents : Continent[]) {
state.continents = continents
}
[Mutations.SetUser]: (state, user : LogOnSuccess) => { state.user = user },
[Mutations.ClearUser]: (state) => { state.user = undefined },
[Mutations.SetLogOnState]: (state, message : string) => { state.logOnState = message },
[Mutations.SetContinents]: (state, continents : Continent[]) => { state.continents = continents },
[Mutations.SetInstances]: (state, instances : Instance[]) => { state.instances = instances }
},
actions: {
async logOn ({ commit }, { abbr, code }) {
[Actions.LogOn]: async ({ commit }, { abbr, code }) => {
const logOnResult = await api.citizen.logOn(abbr, code)
if (typeof logOnResult === "string") {
commit("setLogOnState", logOnResult)
commit(Mutations.SetLogOnState, logOnResult)
} else {
commit("setUser", logOnResult)
commit(Mutations.SetUser, logOnResult)
}
},
async ensureContinents ({ state, commit }) {
[Actions.EnsureContinents]: async ({ state, commit }) => {
if (state.continents.length > 0) return
const theSeven = await api.continent.all()
if (typeof theSeven === "string") {
console.error(theSeven)
} else {
commit("setContinents", theSeven)
commit(Mutations.SetContinents, theSeven)
}
},
[Actions.EnsureInstances]: async ({ state, commit }) => {
if (state.instances.length > 0) return
const instResp = await api.instances.all()
if (typeof instResp === "string") {
console.error(instResp)
} else if (typeof instResp === "undefined") {
console.error("No instances were found; this should not happen")
} else {
commit(Mutations.SetInstances, instResp)
}
}
},
modules: {
}
})
export * as Actions from "./actions"
export * as Mutations from "./mutations"

View File

@ -0,0 +1,14 @@
/** Set the logged-on user */
export const SetUser = "setUser"
/** Clear the logged-on user */
export const ClearUser = "clearUser"
/** Set the status of the current log on action */
export const SetLogOnState = "setLogOnState"
/** Set the list of continents */
export const SetContinents = "setContinents"
/** Set the list of Mastodon instances */
export const SetInstances = "setInstances"

View File

@ -13,14 +13,13 @@ article
h4 Description of Service and Registration
p
| Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles, restricting access
| to the details of these profiles to other users of
| to the details of these profiles to other users of No Agenda-afilliated Mastodon sites (currently
= " "
template(v-for="(it, idx) in instances" :key="idx")
a(:href="it.url" target="_blank") {{it.name}}
template(v-if="idx + 2 < instances.length")= ", "
template(v-else-if="idx + 1 < instances.length")= ", and "
template(v-else)= ". "
| Registration is accomplished by allowing Jobs, Jobs, Jobs to read one&rsquo;s Mastodon profile. See our
| ). Registration is accomplished by allowing Jobs, Jobs, Jobs to read one&rsquo;s Mastodon profile. See our
= " "
router-link(to="/privacy-policy") privacy policy
= " "
@ -51,21 +50,14 @@ article
</template>
<script setup lang="ts">
import { onMounted, Ref, ref } from "vue"
import { computed, onMounted } from "vue"
import { useStore, Actions } from "@/store"
import api, { Instance } from "@/api"
import { toastError } from "@/components/layout/AppToaster.vue"
const store = useStore()
const instances : Ref<Instance[]> = ref([])
/** All instances authorized to view Jobs, Jobs, Jobs */
const instances = computed(() => store.state.instances)
onMounted(async () => { await store.dispatch(Actions.EnsureInstances) })
onMounted(async () => {
const apiResp = await api.instances.all()
if (typeof apiResp === "string") {
toastError(apiResp, "retrieving instances")
} else if (typeof apiResp === "undefined") {
toastError("No instances to display", undefined)
} else {
instances.value = apiResp
}
})
</script>

View File

@ -8,8 +8,7 @@ article
<script setup lang="ts">
import { computed, onMounted } from "vue"
import { useRoute, useRouter } from "vue-router"
import api from "@/api"
import { useStore } from "@/store"
import { useStore, Actions, Mutations } from "@/store"
import { AFTER_LOG_ON_URL } from "@/router"
const store = useStore()
@ -20,20 +19,19 @@ const router = useRouter()
const abbr = route.params.abbr as string
/** Set the message for this component */
const setMessage = (msg : string) => store.commit("setLogOnState", msg)
const setMessage = (msg : string) => store.commit(Mutations.SetLogOnState, msg)
/** Pass the code to the API and exchange it for a user and a JWT */
const logOn = async () => {
const instance = await api.instances.byAbbr(abbr)
if (typeof instance === "string") {
setMessage(instance)
} else if (typeof instance === "undefined") {
await store.dispatch(Actions.EnsureInstances)
const instance = store.state.instances.find(it => it.abbr === abbr)
if (typeof instance === "undefined") {
setMessage(`Mastodon instance ${abbr} not found`)
} else {
setMessage(`<em>Welcome back! Verifying your ${instance.name} account&hellip;</em>`)
const code = route.query.code
if (code) {
await store.dispatch("logOn", { abbr, code })
await store.dispatch(Actions.LogOn, { abbr, code })
if (store.state.user !== undefined) {
const afterLogOnUrl = window.localStorage.getItem(AFTER_LOG_ON_URL)
if (afterLogOnUrl) {

View File

@ -6,7 +6,8 @@ article.container
.col: .card.h-100
h5.card-header Your Profile
.card-body
h6.card-subtitle.mb-3.text-muted.fst-italic Last updated #[full-date-time(:date="profile.lastUpdatedOn")]
h6.card-subtitle.mb-3.text-muted.fst-italic(v-if="profile").
Last updated #[full-date-time(:date="profile.lastUpdatedOn")]
p.card-text(v-if="profile")
| Your profile currently lists {{profile.skills.length}}
| skill#[template(v-if="profile.skills.length !== 1") s].

View File

@ -6,9 +6,9 @@ article
.col-12.col-sm-10.col-md-8.col-lg-6
.form-floating
input.form-control(type="text" id="realName" v-model="v$.realName.$model" maxlength="255"
placeholder="Leave blank to use your NAS display name")
placeholder="Leave blank to use your Mastodon display name")
label(for="realName") Real Name
.form-text Leave blank to use your NAS display name
.form-text Leave blank to use your Mastodon display name
.col-12
.form-check
input.form-check-input(type="checkbox" id="isSeeking" v-model="v$.isSeekingEmployment.$model")

View File

@ -9,13 +9,13 @@ article
import { onMounted } from "vue"
import { useRouter } from "vue-router"
import { toastSuccess } from "@/components/layout/AppToaster.vue"
import { useStore } from "@/store"
import { useStore, Mutations } from "@/store"
const store = useStore()
const router = useRouter()
onMounted(() => {
store.commit("clearUser")
store.commit(Mutations.ClearUser)
toastSuccess("Log Off Successful &nbsp; | &nbsp; <strong>Have a Nice Day!</strong>")
router.push("/")
})

View File

@ -1,22 +1,24 @@
<template lang="pug">
article
p &nbsp;
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}}
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">
import { computed, Ref, ref } from "vue"
import api, { Instance } from "@/api"
import { computed, onMounted, Ref, ref } from "vue"
import { Instance } from "@/api"
import { useStore, Actions } from "@/store"
import LoadData from "@/components/LoadData.vue"
const store = useStore()
/** The instances configured for Jobs, Jobs, Jobs */
const instances : Ref<Instance[]> = ref([])
const instances = computed(() => store.state.instances)
/** Whether authorization is in progress */
const selected : Ref<Instance | undefined> = ref(undefined)
@ -43,15 +45,6 @@ const select = (abbr : string) => {
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
}
}
onMounted(async () => { await store.dispatch(Actions.EnsureInstances) })
</script>

View File

@ -65,7 +65,7 @@ const title = computed(() => it.value ? `${it.value.listing.title} | Job Listing
/** The HTML details of the job listing */
const details = computed(() => toHtml(it.value?.listing.text ?? ""))
/** The NAS profile URL for the citizen who posted this job listing */
/** The Mastodon profile URL for the citizen who posted this job listing */
const profileUrl = computed(() => citizen.value ? citizen.value.profileUrl : "")
/** The needed by date, formatted in SHOUTING MODE */

View File

@ -21,14 +21,14 @@ article
p.text-center: button.btn.btn-danger(@click.prevent="deleteAccount") Delete Your Entire Account
</template>
<script lang="ts">
<script setup lang="ts">
import { onMounted } from "vue"
import { useRouter } from "vue-router"
import api, { LogOnSuccess } from "@/api"
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
import { useStore } from "@/store"
</script>
import { useStore, Actions, Mutations } from "@/store"
<script setup lang="ts">
const store = useStore()
const router = useRouter()
@ -54,21 +54,22 @@ const deleteAccount = async () => {
} else if (typeof citizenResp === "undefined") {
toastError("Could not retrieve citizen record", undefined)
} else {
const instResp = await api.instances.byAbbr(citizenResp.instance)
if (typeof instResp === "string") {
toastError(instResp, "retriving instance")
} else if (typeof instResp === "undefined") {
const instance = store.state.instances.find(it => it.abbr === citizenResp.instance)
if (typeof instance === "undefined") {
toastError("Could not retrieve instance", undefined)
} else {
const resp = await api.citizen.delete(user)
if (typeof resp === "string") {
toastError(resp, "Deleting Account")
} else {
store.commit("clearUser")
store.commit(Mutations.ClearUser)
toastSuccess("Account Deleted Successfully")
router.push(`/so-long/success/${instResp.url}`)
router.push(`/so-long/success/${instance.abbr}`)
}
}
}
}
onMounted(async () => { await store.dispatch(Actions.EnsureInstances) })
</script>

View File

@ -11,11 +11,19 @@ article
</template>
<script setup lang="ts">
import { computed, onMounted } from "vue"
import { useRoute } from "vue-router"
import { useStore, Actions } from "@/store"
const route = useRoute()
const store = useStore()
/** The URL of the instance from which the deleted user had authorized access */
const url = route.params.url as string
/** The abbreviation of the instance from which the deleted user had authorized access */
const abbr = route.params.abbr as string
/** The URL of that instance */
const url = computed(() => store.state.instances.find(it => it.abbr === abbr)?.url ?? "")
onMounted(async () => { await store.dispatch(Actions.EnsureInstances) })
</script>

View File

@ -31,7 +31,7 @@ const user = store.state.user as LogOnSuccess
/** The story to be displayed */
const story : Ref<Success | undefined> = ref(undefined)
/** The citizen's name (real, display, or NAS, whichever is found first) */
/** The citizen's name (real, display, or Mastodon, whichever is found first) */
const citizenName = ref("")
/** Retrieve the success story */