Version 3 #40
|
@ -19,7 +19,8 @@ import {
|
||||||
PublicSearchResult,
|
PublicSearchResult,
|
||||||
StoryEntry,
|
StoryEntry,
|
||||||
StoryForm,
|
StoryForm,
|
||||||
Success
|
Success,
|
||||||
|
Valid
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,6 +111,20 @@ export default {
|
||||||
register: async (form : CitizenRegistrationForm) : Promise<boolean | string> =>
|
register: async (form : CitizenRegistrationForm) : Promise<boolean | string> =>
|
||||||
apiSend(await fetch(apiUrl("citizen/register"), reqInit("POST", undefined, form)), "registering citizen"),
|
apiSend(await fetch(apiUrl("citizen/register"), reqInit("POST", undefined, form)), "registering citizen"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm an account by verifying a token they received via e-mail
|
||||||
|
*
|
||||||
|
* @param token The token to be verified
|
||||||
|
* @return True if the token is value, false if it is not, or an error message if one is encountered
|
||||||
|
*/
|
||||||
|
confirmToken: async (token : string) : Promise<boolean | string> => {
|
||||||
|
const resp = await apiResult<Valid>(
|
||||||
|
await fetch(apiUrl("citizen/confirm"), reqInit("PATCH", undefined, { token })), "confirming account")
|
||||||
|
if (typeof resp === "string") return resp
|
||||||
|
if (typeof resp === "undefined") return false
|
||||||
|
return resp.valid
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a citizen on
|
* Log a citizen on
|
||||||
*
|
*
|
||||||
|
|
|
@ -312,3 +312,9 @@ export interface Success {
|
||||||
/** The success story */
|
/** The success story */
|
||||||
story : string | undefined
|
story : string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether a check is valid */
|
||||||
|
export interface Valid {
|
||||||
|
/** The validity */
|
||||||
|
valid : boolean
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
} from "vue-router"
|
} from "vue-router"
|
||||||
import store, { Mutations } from "@/store"
|
import store, { Mutations } from "@/store"
|
||||||
import Home from "@/views/Home.vue"
|
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 Mastodon */
|
/** 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"
|
export const AFTER_LOG_ON_URL = "jjj-after-log-on-url"
|
||||||
|
@ -35,38 +34,44 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
path: "/how-it-works",
|
path: "/how-it-works",
|
||||||
name: "HowItWorks",
|
name: "HowItWorks",
|
||||||
component: () => import(/* webpackChunkName: "help" */ "../views/HowItWorks.vue"),
|
component: () => import(/* webpackChunkName: "help" */ "../views/HowItWorks.vue"),
|
||||||
meta: { title: "How It Works" }
|
meta: { auth: false, title: "How It Works" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/privacy-policy",
|
path: "/privacy-policy",
|
||||||
name: "PrivacyPolicy",
|
name: "PrivacyPolicy",
|
||||||
component: () => import(/* webpackChunkName: "legal" */ "../views/PrivacyPolicy.vue"),
|
component: () => import(/* webpackChunkName: "legal" */ "../views/PrivacyPolicy.vue"),
|
||||||
meta: { title: "Privacy Policy" }
|
meta: { auth: false, title: "Privacy Policy" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/terms-of-service",
|
path: "/terms-of-service",
|
||||||
name: "TermsOfService",
|
name: "TermsOfService",
|
||||||
component: () => import(/* webpackChunkName: "legal" */ "../views/TermsOfService.vue"),
|
component: () => import(/* webpackChunkName: "legal" */ "../views/TermsOfService.vue"),
|
||||||
meta: { title: "Terms of Service" }
|
meta: { auth: false, title: "Terms of Service" }
|
||||||
},
|
},
|
||||||
// Citizen URLs
|
// Citizen URLs
|
||||||
{
|
{
|
||||||
path: "/citizen/register",
|
path: "/citizen/register",
|
||||||
name: "CitizenRegistration",
|
name: "CitizenRegistration",
|
||||||
component: () => import(/* webpackChunkName: "register" */ "../views/citizen/Register.vue"),
|
component: () => import(/* webpackChunkName: "register" */ "../views/citizen/Register.vue"),
|
||||||
meta: { title: "Register" }
|
meta: { auth: false, title: "Register" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/citizen/registered",
|
path: "/citizen/registered",
|
||||||
name: "CitizenRegistered",
|
name: "CitizenRegistered",
|
||||||
component: () => import(/* webpackChunkName: "register" */ "../views/citizen/Registered.vue"),
|
component: () => import(/* webpackChunkName: "register" */ "../views/citizen/Registered.vue"),
|
||||||
meta: { title: "Registration Successful" }
|
meta: { auth: false, title: "Registration Successful" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/citizen/confirm/:token",
|
||||||
|
name: "ConfirmRegistration",
|
||||||
|
component: () => import(/* webpackChunkName: "logon" */ "../views/citizen/ConfirmRegistration.vue"),
|
||||||
|
meta: { auth: false, title: "Account Confirmation" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/citizen/log-on",
|
path: "/citizen/log-on",
|
||||||
name: "LogOn",
|
name: "LogOn",
|
||||||
component: LogOn,
|
component: () => import(/* webpackChunkName: "logon" */ "../views/citizen/LogOn.vue"),
|
||||||
meta: { title: "Log On" }
|
meta: { auth: false, title: "Log On" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/citizen/:abbr/authorized",
|
path: "/citizen/:abbr/authorized",
|
||||||
|
@ -150,7 +155,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
meta: { auth: true, title: "Account Deletion Options" }
|
meta: { auth: true, title: "Account Deletion Options" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/so-long/success/:abbr",
|
path: "/so-long/success",
|
||||||
name: "DeletionSuccess",
|
name: "DeletionSuccess",
|
||||||
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionSuccess.vue"),
|
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionSuccess.vue"),
|
||||||
meta: { auth: false, title: "Account Deletion Success" }
|
meta: { auth: false, title: "Account Deletion Success" }
|
||||||
|
@ -187,7 +192,7 @@ const router = createRouter({
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
router.beforeEach((to : RouteLocationNormalized, from : RouteLocationNormalized) => {
|
router.beforeEach((to : RouteLocationNormalized, from : RouteLocationNormalized) => {
|
||||||
if (store.state.user === undefined && (to.meta.auth || false)) {
|
if (store.state.user === undefined && to.meta.auth) {
|
||||||
window.localStorage.setItem(AFTER_LOG_ON_URL, to.fullPath)
|
window.localStorage.setItem(AFTER_LOG_ON_URL, to.fullPath)
|
||||||
return "/citizen/log-on"
|
return "/citizen/log-on"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<article>
|
||||||
|
<h3 class="pb-3">Account Confirmation</h3>
|
||||||
|
<load-data :load="confirmToken">
|
||||||
|
<p v-if="isConfirmed">
|
||||||
|
Your account was confirmed successfully! You may <router-link to="/citizen/log-on">log on here</router-link>.
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
The confirmation token did not match any pending accounts. Confirmation tokens are only valid for 3 days; if
|
||||||
|
the token expired, you will need to re-register,
|
||||||
|
which <router-link to="/citzen/register">you can do here</router-link>.
|
||||||
|
</p>
|
||||||
|
</load-data>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
|
||||||
|
import api from "@/api"
|
||||||
|
import LoadData from "@/components/LoadData.vue"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
/** Whether the account was confirmed */
|
||||||
|
const isConfirmed = ref(false)
|
||||||
|
|
||||||
|
/** Confirm the account via the token */
|
||||||
|
const confirmToken = async (errors: string[]) => {
|
||||||
|
const resp = await api.citizen.confirmToken(route.params.token as string)
|
||||||
|
if (typeof resp === "string") {
|
||||||
|
errors.push(resp)
|
||||||
|
} else {
|
||||||
|
isConfirmed.value = resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,29 +1,39 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
page-title(:title="title")
|
<h3 class="pb-3">{{title}}</h3>
|
||||||
load-data(:load="retrieveProfile")
|
<load-data :load="retrieveProfile">
|
||||||
h2
|
<h2>
|
||||||
a(:href="it.citizen.profileUrl" target="_blank") {{citizenName(it.citizen)}}
|
<a :href="it.citizen.profileUrl" target="_blank" rel="noopener">{{citizenName(it.citizen)}}</a>
|
||||||
.jjj-heading-label(v-if="it.profile.seekingEmployment")
|
<span class="jjj-heading-label" v-if="it.profile.seekingEmployment">
|
||||||
| #[span.badge.bg-dark Currently Seeking Employment]
|
<span class="badge bg-dark">Currently Seeking Employment</span>
|
||||||
h4.pb-3 {{it.continent.name}}, {{it.profile.region}}
|
</span>
|
||||||
p(v-html="workTypes")
|
</h2>
|
||||||
hr
|
<h4 class="pb-3">{{it.continent.name}}, {{it.profile.region}}</h4>
|
||||||
div(v-html="bioHtml")
|
<p v-html="workTypes" />
|
||||||
template(v-if="it.profile.skills.length > 0")
|
<hr>
|
||||||
hr
|
<div v-html="bioHtml" />
|
||||||
h4.pb-3 Skills
|
<template v-if="it.profile.skills.length > 0">
|
||||||
ul
|
<hr>
|
||||||
li(v-for="(skill, idx) in it.profile.skills" :key="idx").
|
<h4 class="pb-3">Skills</h4>
|
||||||
{{skill.description}}#[template(v-if="skill.notes") ({{skill.notes}})]
|
<ul>
|
||||||
template(v-if="it.profile.experience")
|
<li v-for="(skill, idx) in it.profile.skills" :key="idx">
|
||||||
hr
|
{{skill.description}}<template v-if="skill.notes"> ({{skill.notes}})</template>
|
||||||
h4.pb-3 Experience / Employment History
|
</li>
|
||||||
div(v-html="expHtml")
|
</ul>
|
||||||
template(v-if="user.citizenId === it.citizen.id")
|
</template>
|
||||||
br
|
<template v-if="it.profile.experience">
|
||||||
br
|
<hr>
|
||||||
router-link.btn.btn-primary(to="/citizen/profile") #[icon(:icon="mdiPencil")] Edit Your Profile
|
<h4 class="pb-3">Experience / Employment History</h4>
|
||||||
|
<div v-html="expHtml" />
|
||||||
|
</template>
|
||||||
|
<template v-if="user.citizenId === it.citizen.id">
|
||||||
|
<br><br>
|
||||||
|
<router-link class="btn btn-primary" to="/citizen/profile">
|
||||||
|
<icon :icon="mdiPencil" /> Edit Your Profile
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</load-data>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
@ -1,32 +1,34 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3.pb-3 Account Deletion Options
|
<h3 class="pb-3">Account Deletion Options</h3>
|
||||||
h4.pb-3 Option 1 – Delete Your Profile
|
<h4 class="pb-3">Option 1 – Delete Your Profile</h4>
|
||||||
p.
|
<p>
|
||||||
Utilizing this option will remove your current employment profile and skills. This will preserve any success stories
|
Utilizing this option will remove your current employment profile and skills. This will preserve any job listings
|
||||||
you may have written, and preserves this application’s knowledge of you. This is what you want to use if you
|
you may have posted, or any success stories you may have written, and preserves this application’s knowledge
|
||||||
want to clear out your profile and start again (and remove the current one from others’ view).
|
of you. This is what you want to use if you want to clear out your profile and start again (and remove the current
|
||||||
p.text-center: button.btn.btn-danger(@click.prevent="deleteProfile") Delete Your Profile
|
one from others’ view).
|
||||||
hr
|
</p>
|
||||||
h4.pb-3 Option 2 – Delete Your Account
|
<p class="text-center">
|
||||||
p.
|
<button class="btn btn-danger" @click.prevent="deleteProfile">Delete Your Profile</button>
|
||||||
This option will make it like you never visited this site. It will delete your profile, skills, success stories, and
|
</p>
|
||||||
account. This is what you want to use if you want to disappear from this application. Clicking the button below
|
<hr>
|
||||||
#[strong will not] affect your Mastodon account in any way; its effects are limited to Jobs, Jobs, Jobs.
|
<h4 class="pb-3">Option 2 – Delete Your Account</h4>
|
||||||
p: em.
|
<p>
|
||||||
(This will not revoke this application’s permissions on Mastodon; you will have to remove this yourself. The
|
This option will make it like you never visited this site. It will delete your profile, skills, job listings,
|
||||||
confirmation message has a link where you can do this; once the page loads, find the
|
success stories, and account. This is what you want to use if you want to disappear from this application.
|
||||||
#[strong Jobs, Jobs, Jobs] entry, and click the #[strong × Revoke] link for that entry.)
|
</p>
|
||||||
p.text-center: button.btn.btn-danger(@click.prevent="deleteAccount") Delete Your Entire Account
|
<p class="text-center">
|
||||||
|
<button class="btn btn-danger" @click.prevent="deleteAccount">Delete Your Entire Account</button>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from "vue"
|
|
||||||
import { useRouter } from "vue-router"
|
import { useRouter } from "vue-router"
|
||||||
|
|
||||||
import api, { LogOnSuccess } from "@/api"
|
import api, { LogOnSuccess } from "@/api"
|
||||||
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||||
import { useStore, Actions, Mutations } from "@/store"
|
import { useStore, Mutations } from "@/store"
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -41,34 +43,19 @@ const deleteProfile = async () => {
|
||||||
toastError(resp, "Deleting Profile")
|
toastError(resp, "Deleting Profile")
|
||||||
} else {
|
} else {
|
||||||
toastSuccess("Profile Deleted Successfully")
|
toastSuccess("Profile Deleted Successfully")
|
||||||
router.push("/citizen/dashboard")
|
await router.push("/citizen/dashboard")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Delete everything pertaining to the user's account */
|
/** Delete everything pertaining to the user's account */
|
||||||
const deleteAccount = async () => {
|
const deleteAccount = async () => {
|
||||||
const citizenResp = await api.citizen.retrieve(user.citizenId, user)
|
|
||||||
if (typeof citizenResp === "string") {
|
|
||||||
toastError(citizenResp, "retrieving citizen")
|
|
||||||
} else if (typeof citizenResp === "undefined") {
|
|
||||||
toastError("Could not retrieve citizen record", undefined)
|
|
||||||
} else {
|
|
||||||
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)
|
const resp = await api.citizen.delete(user)
|
||||||
if (typeof resp === "string") {
|
if (typeof resp === "string") {
|
||||||
toastError(resp, "Deleting Account")
|
toastError(resp, "Deleting Account")
|
||||||
} else {
|
} else {
|
||||||
store.commit(Mutations.ClearUser)
|
store.commit(Mutations.ClearUser)
|
||||||
toastSuccess("Account Deleted Successfully")
|
toastSuccess("Account Deleted Successfully")
|
||||||
router.push(`/so-long/success/${instance.abbr}`)
|
await router.push("/so-long/success")
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => { await store.dispatch(Actions.EnsureInstances) })
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,28 +1,9 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3.pb-3 Account Deletion Success
|
<h3 class="pb-3">Account Deletion Success</h3>
|
||||||
p.
|
<p> </p>
|
||||||
Your account has been successfully deleted. To revoke the permissions you have previously granted to this
|
<p>Your account has been successfully deleted.</p>
|
||||||
application, find it in #[a(:href="`${url}/oauth/authorized_applications`") this list] and click
|
<p> </p>
|
||||||
#[strong × Revoke]. Otherwise, clicking “Log On” in the left-hand menu will create a new, empty
|
<p>Thank you for participating, and thank you for your courage. #GitmoNation</p>
|
||||||
account without prompting you further.
|
</article>
|
||||||
p Thank you for participating, and thank you for your courage. #GitmoNation
|
|
||||||
</template>
|
</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 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>
|
|
||||||
|
|
|
@ -1,20 +1,31 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3.pb-3 {{title}}
|
<h3 class="pb-3">{{title}}</h3>
|
||||||
load-data(:load="retrieveStory")
|
<load-data :load="retrieveStory">
|
||||||
p(v-if="isNew").
|
<p v-if="isNew">
|
||||||
Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came about; tell us
|
Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came about; tell us
|
||||||
about it below! #[em (These will be visible to other users, but not to the general public.)]
|
about it below! <em>(These will be visible to other users, but not to the general public.)</em>
|
||||||
form.row.g-3
|
</p>
|
||||||
.col-12: .form-check
|
<form class="row g-3">
|
||||||
input.form-check-input(type="checkbox" id="fromHere" v-model="v$.fromHere.$model")
|
<div class="col-12">
|
||||||
label.form-check-label(for="fromHere") I found my employment here
|
<div class="form-check">
|
||||||
markdown-editor(id="story" label="The Success Story" v-model:text="v$.story.$model")
|
<input type="checkbox" id="fromHere" class="form-check-input" v-model="v$.fromHere.$model">
|
||||||
.col-12
|
<label class="form-check-label" for="fromHere">I found my employment here</label>
|
||||||
button.btn.btn-primary(type="submit" @click.prevent="saveStory(true)").
|
</div>
|
||||||
#[icon(:icon="mdiContentSaveOutline")] Save
|
</div>
|
||||||
p(v-if="isNew"): em (Saving this will set “Seeking Employment” to “No” on your profile.)
|
<markdown-editor id="story" label="The Success Story" v-model:text="v$.story.$model" />
|
||||||
maybe-save(:saveAction="doSave" :validator="v$")
|
<div class="col-12">
|
||||||
|
<button class="btn btn-primary" type="submit" @click.prevent="saveStory(true)">
|
||||||
|
<icon :icon="mdiContentSaveOutline" /> Save
|
||||||
|
</button>
|
||||||
|
<p v-if="isNew">
|
||||||
|
<em>(Saving this will set “Seeking Employment” to “No” on your profile.)</em>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</load-data>
|
||||||
|
<maybe-save :saveAction="doSave" :validator="v$" />
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -91,14 +102,14 @@ const saveStory = async (navigate : boolean) => {
|
||||||
toastSuccess("Success Story saved and Seeking Employment flag cleared successfully")
|
toastSuccess("Success Story saved and Seeking Employment flag cleared successfully")
|
||||||
v$.value.$reset()
|
v$.value.$reset()
|
||||||
if (navigate) {
|
if (navigate) {
|
||||||
router.push("/success-story/list")
|
await router.push("/success-story/list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toastSuccess("Success Story saved successfully")
|
toastSuccess("Success Story saved successfully")
|
||||||
v$.value.$reset()
|
v$.value.$reset()
|
||||||
if (navigate) {
|
if (navigate) {
|
||||||
router.push("/success-story/list")
|
await router.push("/success-story/list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,34 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
h3.pb-3 Success Stories
|
<h3 class="pb-3">Success Stories</h3>
|
||||||
load-data(:load="retrieveStories")
|
<load-data :load="retrieveStories">
|
||||||
table.table.table-sm.table-hover(v-if="stories?.length > 0")
|
<table class="table table-sm table-hover" v-if="stories?.length > 0">
|
||||||
thead: tr
|
<thead>
|
||||||
th(scope="col") Story
|
<tr>
|
||||||
th(scope="col") From
|
<th scope="col">Story</th>
|
||||||
th(scope="col") Found Here?
|
<th scope="col">From</th>
|
||||||
th(scope="col") Recorded On
|
<th scope="col">Found Here?</th>
|
||||||
tbody: tr(v-for="story in stories" :key="story.id")
|
<th scope="col">Recorded On</th>
|
||||||
td
|
</tr>
|
||||||
router-link(v-if="story.hasStory" :to="`/success-story/${story.id}/view`") View
|
</thead>
|
||||||
em(v-else) None
|
<tbody>
|
||||||
template(v-if="story.citizenId === user.citizenId")
|
<tr v-for="story in stories" :key="story.id">
|
||||||
| ~ #[router-link(:to="`/success-story/${story.id}/edit`") Edit]
|
<td>
|
||||||
td {{story.citizenName}}
|
<router-link v-if="story.hasStory" :to="`/success-story/${story.id}/view`">View</router-link>
|
||||||
td
|
<em v-else>None</em>
|
||||||
strong(v-if="story.fromHere") Yes
|
<template v-if="story.citizenId === user.citizenId">
|
||||||
template(v-else) No
|
~ <router-link :to="`/success-story/${story.id}/edit`">Edit</router-link>
|
||||||
td: full-date(:date="story.recordedOn")
|
</template>
|
||||||
p(v-else) There are no success stories recorded #[em (yet)]
|
</td>
|
||||||
|
<td>{{story.citizenName}}</td>
|
||||||
|
<td><strong v-if="story.fromHere">Yes</strong><template v-else>No</template></td>
|
||||||
|
<td><full-date :date="story.recordedOn" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p v-else>There are no success stories recorded <em>(yet)</em></p>
|
||||||
|
</load-data>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
<template lang="pug">
|
<template>
|
||||||
article
|
<article>
|
||||||
load-data(:load="retrieveStory")
|
<load-data :load="retrieveStory">
|
||||||
h3
|
<h3>
|
||||||
| {{citizenName}}’s Success Story
|
{{citizenName}}’s Success Story
|
||||||
.jjj-heading-label(v-if="story.fromHere")
|
<span class="jjj-heading-label" v-if="story.fromHere">
|
||||||
| #[span.badge.bg-success Via {{profileOrListing}} on Jobs, Jobs, Jobs]
|
<span class="badge bg-success">Via {{profileOrListing}} on Jobs, Jobs, Jobs</span>
|
||||||
h4.pb-3.text-muted: full-date-time(:date="story.recordedOn")
|
</span>
|
||||||
div(v-if="story.story" v-html="successStory")
|
</h3>
|
||||||
|
<h4 class="pb-3 text-muted"><full-date-time :date="story.recordedOn" /></h4>
|
||||||
|
<div v-if="story.story" v-html="successStory" />
|
||||||
|
</load-data>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
@ -138,8 +138,6 @@ open JobsJobsJobs.Domain
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Citizens =
|
module Citizens =
|
||||||
|
|
||||||
open Npgsql
|
|
||||||
|
|
||||||
/// Delete a citizen by their ID
|
/// Delete a citizen by their ID
|
||||||
let deleteById citizenId = backgroundTask {
|
let deleteById citizenId = backgroundTask {
|
||||||
let! _ =
|
let! _ =
|
||||||
|
@ -184,6 +182,37 @@ module Citizens =
|
||||||
do! txn.CommitAsync ()
|
do! txn.CommitAsync ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Purge expired tokens
|
||||||
|
let private purgeExpiredTokens now = backgroundTask {
|
||||||
|
let connProps = connection ()
|
||||||
|
let! info =
|
||||||
|
Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data ->> 'tokenExpires' IS NOT NULL" connProps
|
||||||
|
|> Sql.executeAsync toDocument<SecurityInfo>
|
||||||
|
for expired in info |> List.filter (fun it -> it.TokenExpires.Value < now) do
|
||||||
|
do! saveSecurity { expired with Token = None; TokenUsage = None; TokenExpires = None } connProps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirm a citizen's account
|
||||||
|
let confirmAccount token now = backgroundTask {
|
||||||
|
do! purgeExpiredTokens now
|
||||||
|
let connProps = connection ()
|
||||||
|
let! tryInfo =
|
||||||
|
connProps
|
||||||
|
|> Sql.query $"
|
||||||
|
SELECT *
|
||||||
|
FROM {Table.SecurityInfo}
|
||||||
|
WHERE data ->> 'token' = @token
|
||||||
|
AND data ->> 'tokenUsage' = 'confirm'"
|
||||||
|
|> Sql.parameters [ "@token", Sql.string token ]
|
||||||
|
|> Sql.executeAsync toDocument<SecurityInfo>
|
||||||
|
match List.tryHead tryInfo with
|
||||||
|
| Some info ->
|
||||||
|
do! saveSecurity { info with AccountLocked = false; Token = None; TokenUsage = None; TokenExpires = None }
|
||||||
|
connProps
|
||||||
|
return true
|
||||||
|
| None -> return false
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempt a user log on
|
/// Attempt a user log on
|
||||||
let tryLogOn email (pwCheck : string -> bool) now = backgroundTask {
|
let tryLogOn email (pwCheck : string -> bool) now = backgroundTask {
|
||||||
let connProps = connection ()
|
let connProps = connection ()
|
||||||
|
@ -229,7 +258,7 @@ module Continents =
|
||||||
/// Retrieve all continents
|
/// Retrieve all continents
|
||||||
let all () =
|
let all () =
|
||||||
connection ()
|
connection ()
|
||||||
|> Sql.query $"SELECT * FROM {Table.Continent}"
|
|> Sql.query $"SELECT * FROM {Table.Continent} ORDER BY data ->> 'name'"
|
||||||
|> Sql.executeAsync toDocument<Continent>
|
|> Sql.executeAsync toDocument<Continent>
|
||||||
|
|
||||||
/// Retrieve a continent by its ID
|
/// Retrieve a continent by its ID
|
||||||
|
|
|
@ -136,6 +136,13 @@ module Citizen =
|
||||||
return! ok next ctx
|
return! ok next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PATCH: /api/citizen/confirm
|
||||||
|
let confirmToken : HttpHandler = fun next ctx -> task {
|
||||||
|
let! form = ctx.BindJsonAsync<{| token : string |}> ()
|
||||||
|
let! valid = Citizens.confirmAccount form.token (now ctx)
|
||||||
|
return! json {| valid = valid |} next ctx
|
||||||
|
}
|
||||||
|
|
||||||
// GET: /api/citizen/log-on/[code]
|
// GET: /api/citizen/log-on/[code]
|
||||||
let logOn (abbr, authCode) : HttpHandler = fun next ctx -> task {
|
let logOn (abbr, authCode) : HttpHandler = fun next ctx -> task {
|
||||||
match! Citizens.tryLogOn "to@do.com" (fun _ -> false) (now ctx) with
|
match! Citizens.tryLogOn "to@do.com" (fun _ -> false) (now ctx) with
|
||||||
|
@ -497,6 +504,7 @@ let allEndpoints = [
|
||||||
routef "/log-on/%s/%s" Citizen.logOn
|
routef "/log-on/%s/%s" Citizen.logOn
|
||||||
routef "/%O" Citizen.get
|
routef "/%O" Citizen.get
|
||||||
]
|
]
|
||||||
|
PATCH [ route "/confirm" Citizen.confirmToken ]
|
||||||
POST [ route "/register" Citizen.register ]
|
POST [ route "/register" Citizen.register ]
|
||||||
DELETE [ route "" Citizen.delete ]
|
DELETE [ route "" Citizen.delete ]
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user