Version 3 #40
|
@ -19,7 +19,8 @@ import {
|
|||
PublicSearchResult,
|
||||
StoryEntry,
|
||||
StoryForm,
|
||||
Success
|
||||
Success,
|
||||
Valid
|
||||
} from "./types"
|
||||
|
||||
/**
|
||||
|
@ -110,6 +111,20 @@ export default {
|
|||
register: async (form : CitizenRegistrationForm) : Promise<boolean | string> =>
|
||||
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
|
||||
*
|
||||
|
|
|
@ -312,3 +312,9 @@ export interface Success {
|
|||
/** The success story */
|
||||
story : string | undefined
|
||||
}
|
||||
|
||||
/** Whether a check is valid */
|
||||
export interface Valid {
|
||||
/** The validity */
|
||||
valid : boolean
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
} from "vue-router"
|
||||
import store, { Mutations } 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 Mastodon */
|
||||
export const AFTER_LOG_ON_URL = "jjj-after-log-on-url"
|
||||
|
@ -35,38 +34,44 @@ const routes: Array<RouteRecordRaw> = [
|
|||
path: "/how-it-works",
|
||||
name: "HowItWorks",
|
||||
component: () => import(/* webpackChunkName: "help" */ "../views/HowItWorks.vue"),
|
||||
meta: { title: "How It Works" }
|
||||
meta: { auth: false, title: "How It Works" }
|
||||
},
|
||||
{
|
||||
path: "/privacy-policy",
|
||||
name: "PrivacyPolicy",
|
||||
component: () => import(/* webpackChunkName: "legal" */ "../views/PrivacyPolicy.vue"),
|
||||
meta: { title: "Privacy Policy" }
|
||||
meta: { auth: false, title: "Privacy Policy" }
|
||||
},
|
||||
{
|
||||
path: "/terms-of-service",
|
||||
name: "TermsOfService",
|
||||
component: () => import(/* webpackChunkName: "legal" */ "../views/TermsOfService.vue"),
|
||||
meta: { title: "Terms of Service" }
|
||||
meta: { auth: false, title: "Terms of Service" }
|
||||
},
|
||||
// Citizen URLs
|
||||
{
|
||||
path: "/citizen/register",
|
||||
name: "CitizenRegistration",
|
||||
component: () => import(/* webpackChunkName: "register" */ "../views/citizen/Register.vue"),
|
||||
meta: { title: "Register" }
|
||||
meta: { auth: false, title: "Register" }
|
||||
},
|
||||
{
|
||||
path: "/citizen/registered",
|
||||
name: "CitizenRegistered",
|
||||
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",
|
||||
name: "LogOn",
|
||||
component: LogOn,
|
||||
meta: { title: "Log On" }
|
||||
component: () => import(/* webpackChunkName: "logon" */ "../views/citizen/LogOn.vue"),
|
||||
meta: { auth: false, title: "Log On" }
|
||||
},
|
||||
{
|
||||
path: "/citizen/:abbr/authorized",
|
||||
|
@ -150,7 +155,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||
meta: { auth: true, title: "Account Deletion Options" }
|
||||
},
|
||||
{
|
||||
path: "/so-long/success/:abbr",
|
||||
path: "/so-long/success",
|
||||
name: "DeletionSuccess",
|
||||
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionSuccess.vue"),
|
||||
meta: { auth: false, title: "Account Deletion Success" }
|
||||
|
@ -187,7 +192,7 @@ const router = createRouter({
|
|||
|
||||
// eslint-disable-next-line
|
||||
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)
|
||||
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">
|
||||
article
|
||||
page-title(:title="title")
|
||||
load-data(:load="retrieveProfile")
|
||||
h2
|
||||
a(:href="it.citizen.profileUrl" target="_blank") {{citizenName(it.citizen)}}
|
||||
.jjj-heading-label(v-if="it.profile.seekingEmployment")
|
||||
| #[span.badge.bg-dark Currently Seeking Employment]
|
||||
h4.pb-3 {{it.continent.name}}, {{it.profile.region}}
|
||||
p(v-html="workTypes")
|
||||
hr
|
||||
div(v-html="bioHtml")
|
||||
template(v-if="it.profile.skills.length > 0")
|
||||
hr
|
||||
h4.pb-3 Skills
|
||||
ul
|
||||
li(v-for="(skill, idx) in it.profile.skills" :key="idx").
|
||||
{{skill.description}}#[template(v-if="skill.notes") ({{skill.notes}})]
|
||||
template(v-if="it.profile.experience")
|
||||
hr
|
||||
h4.pb-3 Experience / Employment History
|
||||
div(v-html="expHtml")
|
||||
template(v-if="user.citizenId === it.citizen.id")
|
||||
br
|
||||
br
|
||||
router-link.btn.btn-primary(to="/citizen/profile") #[icon(:icon="mdiPencil")] Edit Your Profile
|
||||
<template>
|
||||
<article>
|
||||
<h3 class="pb-3">{{title}}</h3>
|
||||
<load-data :load="retrieveProfile">
|
||||
<h2>
|
||||
<a :href="it.citizen.profileUrl" target="_blank" rel="noopener">{{citizenName(it.citizen)}}</a>
|
||||
<span class="jjj-heading-label" v-if="it.profile.seekingEmployment">
|
||||
<span class="badge bg-dark">Currently Seeking Employment</span>
|
||||
</span>
|
||||
</h2>
|
||||
<h4 class="pb-3">{{it.continent.name}}, {{it.profile.region}}</h4>
|
||||
<p v-html="workTypes" />
|
||||
<hr>
|
||||
<div v-html="bioHtml" />
|
||||
<template v-if="it.profile.skills.length > 0">
|
||||
<hr>
|
||||
<h4 class="pb-3">Skills</h4>
|
||||
<ul>
|
||||
<li v-for="(skill, idx) in it.profile.skills" :key="idx">
|
||||
{{skill.description}}<template v-if="skill.notes"> ({{skill.notes}})</template>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-if="it.profile.experience">
|
||||
<hr>
|
||||
<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>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -1,32 +1,34 @@
|
|||
<template lang="pug">
|
||||
article
|
||||
h3.pb-3 Account Deletion Options
|
||||
h4.pb-3 Option 1 – Delete Your Profile
|
||||
p.
|
||||
Utilizing this option will remove your current employment profile and skills. This will preserve any success stories
|
||||
you may have written, and preserves this application’s knowledge of you. This is what you want to use if you
|
||||
want to clear out your profile and start again (and remove the current one from others’ view).
|
||||
p.text-center: button.btn.btn-danger(@click.prevent="deleteProfile") Delete Your Profile
|
||||
hr
|
||||
h4.pb-3 Option 2 – Delete Your Account
|
||||
p.
|
||||
This option will make it like you never visited this site. It will delete your profile, skills, success stories, and
|
||||
account. This is what you want to use if you want to disappear from this application. Clicking the button below
|
||||
#[strong will not] affect your Mastodon account in any way; its effects are limited to Jobs, Jobs, Jobs.
|
||||
p: em.
|
||||
(This will not revoke this application’s permissions on Mastodon; you will have to remove this yourself. The
|
||||
confirmation message has a link where you can do this; once the page loads, find the
|
||||
#[strong Jobs, Jobs, Jobs] entry, and click the #[strong × Revoke] link for that entry.)
|
||||
p.text-center: button.btn.btn-danger(@click.prevent="deleteAccount") Delete Your Entire Account
|
||||
<template>
|
||||
<article>
|
||||
<h3 class="pb-3">Account Deletion Options</h3>
|
||||
<h4 class="pb-3">Option 1 – Delete Your Profile</h4>
|
||||
<p>
|
||||
Utilizing this option will remove your current employment profile and skills. This will preserve any job listings
|
||||
you may have posted, or any success stories you may have written, and preserves this application’s knowledge
|
||||
of you. This is what you want to use if you want to clear out your profile and start again (and remove the current
|
||||
one from others’ view).
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<button class="btn btn-danger" @click.prevent="deleteProfile">Delete Your Profile</button>
|
||||
</p>
|
||||
<hr>
|
||||
<h4 class="pb-3">Option 2 – Delete Your Account</h4>
|
||||
<p>
|
||||
This option will make it like you never visited this site. It will delete your profile, skills, job listings,
|
||||
success stories, and account. This is what you want to use if you want to disappear from this application.
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<button class="btn btn-danger" @click.prevent="deleteAccount">Delete Your Entire Account</button>
|
||||
</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<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, Actions, Mutations } from "@/store"
|
||||
import { useStore, Mutations } from "@/store"
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
@ -41,34 +43,19 @@ const deleteProfile = async () => {
|
|||
toastError(resp, "Deleting Profile")
|
||||
} else {
|
||||
toastSuccess("Profile Deleted Successfully")
|
||||
router.push("/citizen/dashboard")
|
||||
await router.push("/citizen/dashboard")
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete everything pertaining to the user's account */
|
||||
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)
|
||||
const resp = await api.citizen.delete(user)
|
||||
if (typeof resp === "string") {
|
||||
toastError(resp, "Deleting Account")
|
||||
} 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)
|
||||
if (typeof resp === "string") {
|
||||
toastError(resp, "Deleting Account")
|
||||
} else {
|
||||
store.commit(Mutations.ClearUser)
|
||||
toastSuccess("Account Deleted Successfully")
|
||||
router.push(`/so-long/success/${instance.abbr}`)
|
||||
}
|
||||
}
|
||||
store.commit(Mutations.ClearUser)
|
||||
toastSuccess("Account Deleted Successfully")
|
||||
await router.push("/so-long/success")
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => { await store.dispatch(Actions.EnsureInstances) })
|
||||
|
||||
</script>
|
||||
|
|
|
@ -1,28 +1,9 @@
|
|||
<template lang="pug">
|
||||
article
|
||||
h3.pb-3 Account Deletion Success
|
||||
p.
|
||||
Your account has been successfully deleted. To revoke the permissions you have previously granted to this
|
||||
application, find it in #[a(:href="`${url}/oauth/authorized_applications`") this list] and click
|
||||
#[strong × Revoke]. Otherwise, clicking “Log On” in the left-hand menu will create a new, empty
|
||||
account without prompting you further.
|
||||
p Thank you for participating, and thank you for your courage. #GitmoNation
|
||||
<template>
|
||||
<article>
|
||||
<h3 class="pb-3">Account Deletion Success</h3>
|
||||
<p> </p>
|
||||
<p>Your account has been successfully deleted.</p>
|
||||
<p> </p>
|
||||
<p>Thank you for participating, and thank you for your courage. #GitmoNation</p>
|
||||
</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 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">
|
||||
article
|
||||
h3.pb-3 {{title}}
|
||||
load-data(:load="retrieveStory")
|
||||
p(v-if="isNew").
|
||||
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.)]
|
||||
form.row.g-3
|
||||
.col-12: .form-check
|
||||
input.form-check-input(type="checkbox" id="fromHere" v-model="v$.fromHere.$model")
|
||||
label.form-check-label(for="fromHere") I found my employment here
|
||||
markdown-editor(id="story" label="The Success Story" v-model:text="v$.story.$model")
|
||||
.col-12
|
||||
button.btn.btn-primary(type="submit" @click.prevent="saveStory(true)").
|
||||
#[icon(:icon="mdiContentSaveOutline")] Save
|
||||
p(v-if="isNew"): em (Saving this will set “Seeking Employment” to “No” on your profile.)
|
||||
maybe-save(:saveAction="doSave" :validator="v$")
|
||||
<template>
|
||||
<article>
|
||||
<h3 class="pb-3">{{title}}</h3>
|
||||
<load-data :load="retrieveStory">
|
||||
<p v-if="isNew">
|
||||
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.)</em>
|
||||
</p>
|
||||
<form class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="fromHere" class="form-check-input" v-model="v$.fromHere.$model">
|
||||
<label class="form-check-label" for="fromHere">I found my employment here</label>
|
||||
</div>
|
||||
</div>
|
||||
<markdown-editor id="story" label="The Success Story" v-model:text="v$.story.$model" />
|
||||
<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>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -91,14 +102,14 @@ const saveStory = async (navigate : boolean) => {
|
|||
toastSuccess("Success Story saved and Seeking Employment flag cleared successfully")
|
||||
v$.value.$reset()
|
||||
if (navigate) {
|
||||
router.push("/success-story/list")
|
||||
await router.push("/success-story/list")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toastSuccess("Success Story saved successfully")
|
||||
v$.value.$reset()
|
||||
if (navigate) {
|
||||
router.push("/success-story/list")
|
||||
await router.push("/success-story/list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,34 @@
|
|||
<template lang="pug">
|
||||
article
|
||||
h3.pb-3 Success Stories
|
||||
load-data(:load="retrieveStories")
|
||||
table.table.table-sm.table-hover(v-if="stories?.length > 0")
|
||||
thead: tr
|
||||
th(scope="col") Story
|
||||
th(scope="col") From
|
||||
th(scope="col") Found Here?
|
||||
th(scope="col") Recorded On
|
||||
tbody: tr(v-for="story in stories" :key="story.id")
|
||||
td
|
||||
router-link(v-if="story.hasStory" :to="`/success-story/${story.id}/view`") View
|
||||
em(v-else) None
|
||||
template(v-if="story.citizenId === user.citizenId")
|
||||
| ~ #[router-link(:to="`/success-story/${story.id}/edit`") Edit]
|
||||
td {{story.citizenName}}
|
||||
td
|
||||
strong(v-if="story.fromHere") Yes
|
||||
template(v-else) No
|
||||
td: full-date(:date="story.recordedOn")
|
||||
p(v-else) There are no success stories recorded #[em (yet)]
|
||||
<template>
|
||||
<article>
|
||||
<h3 class="pb-3">Success Stories</h3>
|
||||
<load-data :load="retrieveStories">
|
||||
<table class="table table-sm table-hover" v-if="stories?.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Story</th>
|
||||
<th scope="col">From</th>
|
||||
<th scope="col">Found Here?</th>
|
||||
<th scope="col">Recorded On</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="story in stories" :key="story.id">
|
||||
<td>
|
||||
<router-link v-if="story.hasStory" :to="`/success-story/${story.id}/view`">View</router-link>
|
||||
<em v-else>None</em>
|
||||
<template v-if="story.citizenId === user.citizenId">
|
||||
~ <router-link :to="`/success-story/${story.id}/edit`">Edit</router-link>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
<template lang="pug">
|
||||
article
|
||||
load-data(:load="retrieveStory")
|
||||
h3
|
||||
| {{citizenName}}’s Success Story
|
||||
.jjj-heading-label(v-if="story.fromHere")
|
||||
| #[span.badge.bg-success Via {{profileOrListing}} on Jobs, Jobs, Jobs]
|
||||
h4.pb-3.text-muted: full-date-time(:date="story.recordedOn")
|
||||
div(v-if="story.story" v-html="successStory")
|
||||
<template>
|
||||
<article>
|
||||
<load-data :load="retrieveStory">
|
||||
<h3>
|
||||
{{citizenName}}’s Success Story
|
||||
<span class="jjj-heading-label" v-if="story.fromHere">
|
||||
<span class="badge bg-success">Via {{profileOrListing}} on Jobs, Jobs, Jobs</span>
|
||||
</span>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -138,8 +138,6 @@ open JobsJobsJobs.Domain
|
|||
[<RequireQualifiedAccess>]
|
||||
module Citizens =
|
||||
|
||||
open Npgsql
|
||||
|
||||
/// Delete a citizen by their ID
|
||||
let deleteById citizenId = backgroundTask {
|
||||
let! _ =
|
||||
|
@ -184,6 +182,37 @@ module Citizens =
|
|||
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
|
||||
let tryLogOn email (pwCheck : string -> bool) now = backgroundTask {
|
||||
let connProps = connection ()
|
||||
|
@ -229,7 +258,7 @@ module Continents =
|
|||
/// Retrieve all continents
|
||||
let all () =
|
||||
connection ()
|
||||
|> Sql.query $"SELECT * FROM {Table.Continent}"
|
||||
|> Sql.query $"SELECT * FROM {Table.Continent} ORDER BY data ->> 'name'"
|
||||
|> Sql.executeAsync toDocument<Continent>
|
||||
|
||||
/// Retrieve a continent by its ID
|
||||
|
|
|
@ -136,6 +136,13 @@ module Citizen =
|
|||
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]
|
||||
let logOn (abbr, authCode) : HttpHandler = fun next ctx -> task {
|
||||
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 "/%O" Citizen.get
|
||||
]
|
||||
PATCH [ route "/confirm" Citizen.confirmToken ]
|
||||
POST [ route "/register" Citizen.register ]
|
||||
DELETE [ route "" Citizen.delete ]
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue
Block a user