Fix final issues with multi-instance (#22)
This commit is contained in:
parent
989beb0659
commit
df6188dfd1
|
@ -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 =
|
||||
|
|
|
@ -202,14 +202,6 @@ module Instances =
|
|||
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
|
||||
[<RequireQualifiedAccess>]
|
||||
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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")
|
||||
},
|
||||
|
|
8
src/JobsJobsJobs/App/src/store/actions.ts
Normal file
8
src/JobsJobsJobs/App/src/store/actions.ts
Normal 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"
|
|
@ -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"
|
||||
|
|
14
src/JobsJobsJobs/App/src/store/mutations.ts
Normal file
14
src/JobsJobsJobs/App/src/store/mutations.ts
Normal 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"
|
|
@ -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’s Mastodon profile. See our
|
||||
| ). Registration is accomplished by allowing Jobs, Jobs, Jobs to read one’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>
|
||||
|
|
|
@ -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…</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) {
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 | <strong>Have a Nice Day!</strong>")
|
||||
router.push("/")
|
||||
})
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
<template lang="pug">
|
||||
article
|
||||
p
|
||||
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…
|
||||
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…
|
||||
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>
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 */
|
||||
|
|
Loading…
Reference in New Issue
Block a user