Version 3 #40
@ -1,4 +1,5 @@
|
||||
import {
|
||||
AccountProfileForm,
|
||||
Citizen,
|
||||
CitizenRegistrationForm,
|
||||
Continent,
|
||||
@ -165,6 +166,16 @@ export default {
|
||||
retrieve: async (id : string, user : LogOnSuccess) : Promise<Citizen | string | undefined> =>
|
||||
apiResult<Citizen>(await fetch(apiUrl(`citizen/${id}`), reqInit("GET", user)), `retrieving citizen ${id}`),
|
||||
|
||||
/**
|
||||
* Save a citizen's account profile
|
||||
*
|
||||
* @param form The data to be saved
|
||||
* @param user The currently logged-on user
|
||||
* @returns True if successful, an error message if not
|
||||
*/
|
||||
save: async (form : AccountProfileForm, user : LogOnSuccess) : Promise<boolean | string> =>
|
||||
apiSend(await fetch(apiUrl("citizen/account"), reqInit("PATCH", user, form)), "saving account profile"),
|
||||
|
||||
/**
|
||||
* Delete the current citizen's entire Jobs, Jobs, Jobs record
|
||||
*
|
||||
|
@ -1,3 +1,32 @@
|
||||
/** An "other contact" for a citizen */
|
||||
export interface OtherContact {
|
||||
/** The ID of this contact */
|
||||
id : string
|
||||
/** The contact type (uses server-side DU) */
|
||||
contactType : string
|
||||
/** The name for this contact */
|
||||
name : string | undefined
|
||||
/** The value of the contact (e-mail, phone number, URL, etc.) */
|
||||
value : string
|
||||
/** Whether this contact is visible on public employment profiles and job listings */
|
||||
isPublic : boolean
|
||||
}
|
||||
|
||||
/** The data a logged-on citizen can update */
|
||||
export class AccountProfileForm {
|
||||
/** The citizen's first name */
|
||||
firstName = ""
|
||||
/** The citizen's last name */
|
||||
lastName = ""
|
||||
/** The name by which the citizen wishes to be known within the site */
|
||||
displayName : string | undefined
|
||||
/** The new password for the citizen */
|
||||
newPassword : string | undefined
|
||||
/** The confirmed new password for the citizen */
|
||||
newPasswordConfirm : string | undefined
|
||||
/** The other contacts for this citizen */
|
||||
contacts : OtherContact[] = []
|
||||
}
|
||||
|
||||
/** A user of Jobs, Jobs, Jobs */
|
||||
export interface Citizen {
|
||||
@ -15,8 +44,8 @@ export interface Citizen {
|
||||
lastName : string
|
||||
/** The citizen's display name */
|
||||
displayName : string | undefined
|
||||
/** The user's real name */
|
||||
otherContacts : any[]
|
||||
/** The citizen's contact information */
|
||||
otherContacts : OtherContact[]
|
||||
}
|
||||
|
||||
/** The data required to register as a user */
|
||||
@ -28,7 +57,7 @@ export class CitizenRegistrationForm {
|
||||
/** The citizen's e-mail address */
|
||||
email = ""
|
||||
/** The name by which the citizen wishes to be known within the site */
|
||||
displayName = ""
|
||||
displayName : string | undefined
|
||||
/** The password for the citizen */
|
||||
password = ""
|
||||
/** The confirmed password for the citizen */
|
||||
|
74
src/JobsJobsJobs/App/src/components/citizen/ContactEdit.vue
Normal file
74
src/JobsJobsJobs/App/src/components/citizen/ContactEdit.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="row pb-3">
|
||||
<div class="col-2 col-md-1 align-self-center">
|
||||
<button class="btn btn-sm btn-outline-danger rounded-pill" title="Delete"
|
||||
@click.prevent="$emit('remove')"> − </button>
|
||||
</div>
|
||||
<div class="col-10 col-md-6">
|
||||
<div class="form-floating">
|
||||
<select :id="`contactType${contact.id}`" class="form-control" :value="contact.contactType"
|
||||
@input="updateValue('contactType', $event.target.value)">
|
||||
<option value="Website">Website</option>
|
||||
<option value="Email">E-mail Address</option>
|
||||
<option value="Phone">Phone Number</option>
|
||||
</select>
|
||||
<label class="jjj-label" :for="`contactType${contact.id}`">Type</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<div class="form-floating">
|
||||
<input type="text" :id="`contactName${contact.id}`" class="form-control" maxlength="1000"
|
||||
placeholder="The name of this contact" :value="contact.name"
|
||||
@input="updateValue('name', $event.target.value)">
|
||||
<label class="jjj-label" :for="`contactName${contact.id}`">Name</label>
|
||||
</div>
|
||||
<div class="form-text">Optional; will link sites and e-mail, qualify phone numbers</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<div class="form-floating">
|
||||
<input type="text" :id="`contactValue${contact.id}`" class="form-control" maxlength="1000"
|
||||
placeholder="The value forthis contact" :value="contact.value"
|
||||
@input="updateValue('value', $event.target.value)">
|
||||
<label class="jjj-label" :for="`contactValue${contact.id}`">Contact</label>
|
||||
</div>
|
||||
<div class="form-text">The URL, e-mail address, or phone number</div>
|
||||
</div>
|
||||
<div class="col-12 col-offset-md-2 col-md-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" :id="`contactIsPublic${contact.id}`" class="form-check-input" value="true"
|
||||
:checked="contact.isPublic">
|
||||
<label class="form-check-label" :for="`contactIsPublic${contact.id}`">Public</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { OtherContact } from "@/api"
|
||||
import { ref, Ref } from "vue"
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: OtherContact
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input") : void
|
||||
(e: "remove") : void
|
||||
(e: "update:modelValue", value: OtherContact) : void
|
||||
}>()
|
||||
|
||||
/** The contact being edited */
|
||||
const contact : Ref<OtherContact> = ref({ ...props.modelValue as OtherContact })
|
||||
|
||||
/** Update a value in the model */
|
||||
const updateValue = (key : string, value : string) => {
|
||||
contact.value = { ...contact.value, [key]: value }
|
||||
emit("update:modelValue", contact.value)
|
||||
emit("input")
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -14,6 +14,9 @@
|
||||
<icon :icon="mdiThumbUp" /> Success Stories
|
||||
</router-link>
|
||||
<div class="separator"></div>
|
||||
<router-link to="/citizen/account" @click="hide">
|
||||
<icon :icon="mdiAccountEdit" /> My Account
|
||||
</router-link>
|
||||
<router-link to="/listings/mine" @click="hide">
|
||||
<icon :icon="mdiSignText" /> My Job Listings
|
||||
</router-link>
|
||||
@ -46,8 +49,8 @@
|
||||
import { computed } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { Offcanvas } from "bootstrap"
|
||||
import { useStore } from "@/store"
|
||||
import {
|
||||
mdiAccountEdit,
|
||||
mdiHelpCircleOutline,
|
||||
mdiHome,
|
||||
mdiLoginVariant,
|
||||
@ -60,6 +63,10 @@ import {
|
||||
mdiViewListOutline
|
||||
} from "@mdi/js"
|
||||
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import Icon from "@/components/Icon.vue"
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
|
@ -85,6 +85,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Dashboard.vue"),
|
||||
meta: { auth: true, title: "Dashboard" }
|
||||
},
|
||||
{
|
||||
path: "/citizen/account",
|
||||
name: "AccountProfile",
|
||||
component: () => import(/* webpackChunkName: "account" */ "../views/citizen/AccountProfile.vue"),
|
||||
meta: { auth: true, title: "Account Profile" }
|
||||
},
|
||||
{
|
||||
path: "/citizen/log-off",
|
||||
name: "LogOff",
|
||||
|
181
src/JobsJobsJobs/App/src/views/citizen/AccountProfile.vue
Normal file
181
src/JobsJobsJobs/App/src/views/citizen/AccountProfile.vue
Normal file
@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<article>
|
||||
<h3 class="pb-3">Account Profile</h3>
|
||||
<p>
|
||||
This information is visible to all fellow logged-on citizens. For publicly-visible employment profiles and job
|
||||
listings, the “Display Name” fields and any public contacts will be displayed.
|
||||
</p>
|
||||
<load-data :load="retrieveData">
|
||||
<form class="row g-3" novalidate>
|
||||
<div class="col-6 col-xl-4">
|
||||
<div class="form-floating has-validation">
|
||||
<input type="text" id="firstName" :class="{ 'form-control': true, 'is-invalid': v$.firstName.$error }"
|
||||
v-model="v$.firstName.$model" placeholder="First Name">
|
||||
<div class="invalid-feedback">Please enter your first name</div>
|
||||
<label class="jjj-required" for="firstName">First Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" id="lastName" :class="{ 'form-control': true, 'is-invalid': v$.lastName.$error }"
|
||||
v-model="v$.lastName.$model" placeholder="Last Name">
|
||||
<div class="invalid-feedback">Please enter your last name</div>
|
||||
<label class="jjj-required" for="firstName">Last Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" id="displayName" class="form-control" v-model="v$.displayName.$model"
|
||||
placeholder="Display Name">
|
||||
<label for="displayName">Display Name</label>
|
||||
<div class="form-text"><em>Optional; overrides first/last for display</em></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-4">
|
||||
<div class="form-floating">
|
||||
<input type="password" id="newPassword"
|
||||
:class="{ 'form-control': true, 'is-invalid': v$.newPassword.$error }"
|
||||
v-model="v$.newPassword.$model" placeholder="Password">
|
||||
<div class="invalid-feedback">Password must be at least 8 characters long</div>
|
||||
<label for="newPassword">New Password</label>
|
||||
</div>
|
||||
<div class="form-text">Leave blank to keep your current password</div>
|
||||
</div>
|
||||
<div class="col-6 col-xl-4">
|
||||
<div class="form-floating">
|
||||
<input type="password" id="newPasswordConfirm"
|
||||
:class="{ 'form-control': true, 'is-invalid': v$.newPasswordConfirm.$error }"
|
||||
v-model="v$.newPasswordConfirm.$model" placeholder="Confirm Password">
|
||||
<div class="invalid-feedback">The passwords do not match</div>
|
||||
<label for="newPasswordConfirm">Confirm New Password</label>
|
||||
</div>
|
||||
<div class="form-text">Leave blank to keep your current password</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<hr>
|
||||
<h4 class="pb-2">
|
||||
Ways to Be Contacted
|
||||
<button class="btn btn-sm btn-outline-primary rounded-pill" @click.prevent="addContact">
|
||||
Add a Contact Method
|
||||
</button>
|
||||
</h4>
|
||||
</div>
|
||||
<contact-edit v-for="(contact, idx) in accountForm.contacts" :key="contact.id"
|
||||
v-model="accountForm.contacts[idx]" @remove="removeContact(contact.id)"
|
||||
@input="v$.contacts.$touch" />
|
||||
<div class="col-12">
|
||||
<p v-if="v$.$error" class="text-danger">Please correct the errors above</p>
|
||||
<button class="btn btn-primary" @click.prevent="saveAccount(false)">
|
||||
<icon :icon="mdiContentSaveOutline" /> Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</load-data>
|
||||
<hr>
|
||||
<p class="text-muted fst-italic">
|
||||
(If you want to delete your profile, or your entire account,
|
||||
<router-link to="/so-long/options">see your deletion options here</router-link>.)
|
||||
</p>
|
||||
<maybe-save :saveAction="() => saveAccount(true)" :validator="v$" />
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from "vue"
|
||||
import { mdiContentSaveOutline } from "@mdi/js"
|
||||
import useVuelidate from "@vuelidate/core"
|
||||
import { minLength, required, sameAs } from "@vuelidate/validators"
|
||||
|
||||
import api, { AccountProfileForm, Citizen, LogOnSuccess } from "@/api"
|
||||
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
import MaybeSave from "@/components/MaybeSave.vue"
|
||||
import ContactEdit from "@/components/citizen/ContactEdit.vue"
|
||||
|
||||
const store = useStore()
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** The information available to update */
|
||||
const accountForm = reactive(new AccountProfileForm())
|
||||
|
||||
/** The validation rules for the form */
|
||||
const rules = computed(() => ({
|
||||
firstName: { required },
|
||||
lastName: { required },
|
||||
displayName: { },
|
||||
newPassword: { length: minLength(8) },
|
||||
newPasswordConfirm: { matchPassword: sameAs(accountForm.newPassword) },
|
||||
contacts: { }
|
||||
}))
|
||||
|
||||
/** Initialize form validation */
|
||||
const v$ = useVuelidate(rules, accountForm, { $lazy: true })
|
||||
|
||||
/** The ID for new contacts */
|
||||
let newContactId = 0
|
||||
|
||||
/** Add a contact to the profile */
|
||||
const addContact = () => {
|
||||
accountForm.contacts.push({
|
||||
id: `new${newContactId++}`,
|
||||
contactType: "Website",
|
||||
name: undefined,
|
||||
value: "",
|
||||
isPublic: false
|
||||
})
|
||||
v$.value.contacts.$touch()
|
||||
}
|
||||
|
||||
/** Remove the given contact from the profile */
|
||||
const removeContact = (contactId : string) => {
|
||||
accountForm.contacts = accountForm.contacts.filter(c => c.id !== contactId)
|
||||
v$.value.contacts.$touch()
|
||||
}
|
||||
|
||||
/** Retrieve the account profile */
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
const citizenResult = await api.citizen.retrieve(user.citizenId, user)
|
||||
if (typeof citizenResult === "string") {
|
||||
errors.push(citizenResult)
|
||||
} else if (typeof citizenResult === "undefined") {
|
||||
errors.push("Citizen not found")
|
||||
} else {
|
||||
// Update the empty form with appropriate values
|
||||
const c = citizenResult as Citizen
|
||||
accountForm.firstName = c.firstName
|
||||
accountForm.lastName = c.lastName
|
||||
accountForm.displayName = c.displayName
|
||||
accountForm.contacts = c.otherContacts
|
||||
}
|
||||
}
|
||||
|
||||
/** Save the account profile */
|
||||
const saveAccount = async (isNavigating: boolean) => {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$error) return
|
||||
// Remove any blank contacts before submitting
|
||||
accountForm.contacts = accountForm.contacts.filter(c => !((c.name?.trim() ?? "") === "" && c.value.trim() === ""))
|
||||
const saveResult = await api.citizen.save(accountForm, user)
|
||||
if (typeof saveResult === "string") {
|
||||
toastError(saveResult, "saving profile")
|
||||
} else {
|
||||
toastSuccess("Account Profile Saved Successfully")
|
||||
if (!isNavigating) {
|
||||
v$.value.$reset()
|
||||
const errors: string[] = []
|
||||
await retrieveData(errors)
|
||||
if (errors.length > 0) {
|
||||
toastError(errors[0], "retrieving updated profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -13,7 +13,7 @@
|
||||
<div class="form-floating">
|
||||
<div class="form-floating">
|
||||
<input type="email" id="email" :class="{ 'form-control': true, 'is-invalid': v$.email.$error }"
|
||||
v-model="v$.email.$model" placeholder="E-mail Address">
|
||||
v-model="v$.email.$model" placeholder="E-mail Address" autofocus>
|
||||
<div class="invalid-feedback">Please enter a valid e-mail address</div>
|
||||
<label class="jjj-required" for="email">E-mail Address</label>
|
||||
</div>
|
||||
|
@ -46,7 +46,7 @@
|
||||
<hr>
|
||||
<h4 class="pb-2">
|
||||
Skills
|
||||
<button class="btn btn-sm btn-outline-primary.rounded-pill" @click.prevent="addSkill">Add a Skill</button>
|
||||
<button class="btn btn-sm btn-outline-primary rounded-pill" @click.prevent="addSkill">Add a Skill</button>
|
||||
</h4>
|
||||
</div>
|
||||
<profile-skill-edit v-for="(skill, idx) in profile.skills" :key="skill.id" v-model="profile.skills[idx]"
|
||||
|
@ -4,7 +4,7 @@ open System.Text.Json
|
||||
open System.Text.Json.Serialization
|
||||
open JobsJobsJobs.Domain
|
||||
|
||||
/// Convert a wrapped GUID to/from its string representation
|
||||
/// Convert a wrapped DU to/from its string representation
|
||||
type WrappedJsonConverter<'T> (wrap : string -> 'T, unwrap : 'T -> string) =
|
||||
inherit JsonConverter<'T> ()
|
||||
override _.Read(reader, _, _) =
|
||||
@ -18,12 +18,14 @@ open NodaTime.Serialization.SystemTextJson
|
||||
/// JsonSerializer options that use the custom converters
|
||||
let options =
|
||||
let opts = JsonSerializerOptions ()
|
||||
[ WrappedJsonConverter (CitizenId.ofString, CitizenId.toString) :> JsonConverter
|
||||
WrappedJsonConverter (ContinentId.ofString, ContinentId.toString)
|
||||
WrappedJsonConverter (ListingId.ofString, ListingId.toString)
|
||||
WrappedJsonConverter (Text, MarkdownString.toString)
|
||||
WrappedJsonConverter (SkillId.ofString, SkillId.toString)
|
||||
WrappedJsonConverter (SuccessId.ofString, SuccessId.toString)
|
||||
[ WrappedJsonConverter (CitizenId.ofString, CitizenId.toString) :> JsonConverter
|
||||
WrappedJsonConverter (ContactType.parse, ContactType.toString)
|
||||
WrappedJsonConverter (ContinentId.ofString, ContinentId.toString)
|
||||
WrappedJsonConverter (ListingId.ofString, ListingId.toString)
|
||||
WrappedJsonConverter (Text, MarkdownString.toString)
|
||||
WrappedJsonConverter (OtherContactId.ofString, OtherContactId.toString)
|
||||
WrappedJsonConverter (SkillId.ofString, SkillId.toString)
|
||||
WrappedJsonConverter (SuccessId.ofString, SuccessId.toString)
|
||||
JsonFSharpConverter ()
|
||||
]
|
||||
|> List.iter opts.Converters.Add
|
||||
|
@ -5,6 +5,47 @@ open JobsJobsJobs.Domain
|
||||
open Microsoft.Extensions.Options
|
||||
open NodaTime
|
||||
|
||||
/// The data to add or update an other contact
|
||||
type OtherContactForm =
|
||||
{ /// The ID of the contact
|
||||
Id : string
|
||||
|
||||
/// The type of the contact
|
||||
ContactType : string
|
||||
|
||||
/// The name of the contact
|
||||
Name : string option
|
||||
|
||||
/// The value of the contact (URL, e-mail address, phone, etc.)
|
||||
Value : string
|
||||
|
||||
/// Whether this contact is displayed for public employment profiles and job listings
|
||||
IsPublic : bool
|
||||
}
|
||||
|
||||
|
||||
/// The data available to update an account profile
|
||||
type AccountProfileForm =
|
||||
{ /// The first name of the citizen
|
||||
FirstName : string
|
||||
|
||||
/// The last name of the citizen
|
||||
LastName : string
|
||||
|
||||
/// The display name for the citizen
|
||||
DisplayName : string option
|
||||
|
||||
/// The citizen's new password
|
||||
NewPassword : string option
|
||||
|
||||
/// Confirmation of the citizen's new password
|
||||
NewPasswordConfirm : string option
|
||||
|
||||
/// The contacts for this profile
|
||||
Contacts : OtherContactForm list
|
||||
}
|
||||
|
||||
|
||||
/// The data required to register a new citizen (user)
|
||||
type CitizenRegistrationForm =
|
||||
{ /// The first name of the new citizen
|
||||
@ -14,7 +55,7 @@ type CitizenRegistrationForm =
|
||||
LastName : string
|
||||
|
||||
/// The display name for the new citizen
|
||||
DisplayName : string
|
||||
DisplayName : string option
|
||||
|
||||
/// The citizen's e-mail address
|
||||
Email : string
|
||||
@ -26,6 +67,7 @@ type CitizenRegistrationForm =
|
||||
ConfirmPassword : string
|
||||
}
|
||||
|
||||
|
||||
/// The data required to add or edit a job listing
|
||||
type ListingForm =
|
||||
{ /// The ID of the listing
|
||||
|
@ -78,6 +78,22 @@ module MarkdownString =
|
||||
let toString = function Text text -> text
|
||||
|
||||
|
||||
/// The ID of an other contact
|
||||
type OtherContactId = OtherContactId of Guid
|
||||
|
||||
/// Support functions for other contact IDs
|
||||
module OtherContactId =
|
||||
|
||||
/// Create a new job listing ID
|
||||
let create () = (Guid.NewGuid >> OtherContactId) ()
|
||||
|
||||
/// A string representation of a listing ID
|
||||
let toString = function OtherContactId it -> ShortGuid.fromGuid it
|
||||
|
||||
/// Parse a string into a listing ID
|
||||
let ofString = ShortGuid.toGuid >> OtherContactId
|
||||
|
||||
|
||||
/// Types of contacts supported by Jobs, Jobs, Jobs
|
||||
type ContactType =
|
||||
/// E-mail addresses
|
||||
@ -87,10 +103,31 @@ type ContactType =
|
||||
/// Websites (personal, social, etc.)
|
||||
| Website
|
||||
|
||||
/// Functions to support contact types
|
||||
module ContactType =
|
||||
|
||||
/// Parse a contact type from a string
|
||||
let parse typ =
|
||||
match typ with
|
||||
| "Email" -> Email
|
||||
| "Phone" -> Phone
|
||||
| "Website" -> Website
|
||||
| it -> invalidOp $"{it} is not a valid contact type"
|
||||
|
||||
/// Convert a contact type to its string representation
|
||||
let toString =
|
||||
function
|
||||
| Email -> "Email"
|
||||
| Phone -> "Phone"
|
||||
| Website -> "Website"
|
||||
|
||||
|
||||
/// Another way to contact a citizen from this site
|
||||
type OtherContact =
|
||||
{ /// The type of contact
|
||||
{ /// The ID of the contact
|
||||
Id : OtherContactId
|
||||
|
||||
/// The type of contact
|
||||
ContactType : ContactType
|
||||
|
||||
/// The name of the contact (Email, No Agenda Social, LinkedIn, etc.)
|
||||
@ -98,6 +135,9 @@ type OtherContact =
|
||||
|
||||
/// The value for the contact (e-mail address, user name, URL, etc.)
|
||||
Value : string
|
||||
|
||||
/// Whether this contact is visible in public employment profiles and job listings
|
||||
IsPublic : bool
|
||||
}
|
||||
|
||||
|
||||
@ -115,9 +155,6 @@ module SkillId =
|
||||
|
||||
/// Parse a string into a skill ID
|
||||
let ofString = ShortGuid.toGuid >> SkillId
|
||||
|
||||
/// Get the GUID value of a skill ID
|
||||
let value = function SkillId guid -> guid
|
||||
|
||||
|
||||
/// The ID of a success report
|
||||
|
@ -1,5 +1,6 @@
|
||||
module JobsJobsJobs.Api.Email
|
||||
|
||||
open System.Net
|
||||
open JobsJobsJobs.Domain
|
||||
open MailKit.Net.Smtp
|
||||
open MailKit.Security
|
||||
@ -8,7 +9,7 @@ open MimeKit
|
||||
/// Send an account confirmation e-mail
|
||||
let sendAccountConfirmation citizen security = backgroundTask {
|
||||
let name = Citizen.name citizen
|
||||
let token = security.Token.Value
|
||||
let token = WebUtility.UrlEncode security.Token.Value
|
||||
use client = new SmtpClient ()
|
||||
do! client.ConnectAsync ("localhost", 25, SecureSocketOptions.None)
|
||||
|
||||
|
@ -116,7 +116,7 @@ module Citizen =
|
||||
Email = form.Email
|
||||
FirstName = form.FirstName
|
||||
LastName = form.LastName
|
||||
DisplayName = noneIfEmpty form.DisplayName
|
||||
DisplayName = noneIfBlank form.DisplayName
|
||||
JoinedOn = now
|
||||
LastSeenOn = now
|
||||
}
|
||||
@ -171,10 +171,38 @@ module Citizen =
|
||||
// GET: /api/citizen/[id]
|
||||
let get citizenId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Citizens.findById (CitizenId citizenId) with
|
||||
| Some citizen -> return! json citizen next ctx
|
||||
| Some citizen -> return! json { citizen with PasswordHash = "" } next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
||||
// PATCH: /api/citizen/account
|
||||
let account : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let! form = ctx.BindJsonAsync<AccountProfileForm> ()
|
||||
match! Citizens.findById (currentCitizenId ctx) with
|
||||
| Some citizen ->
|
||||
let password =
|
||||
if defaultArg form.NewPassword "" = "" then citizen.PasswordHash
|
||||
else Auth.Passwords.hash citizen form.NewPassword.Value
|
||||
do! Citizens.save
|
||||
{ citizen with
|
||||
FirstName = form.FirstName
|
||||
LastName = form.LastName
|
||||
DisplayName = noneIfBlank form.DisplayName
|
||||
PasswordHash = password
|
||||
OtherContacts = form.Contacts
|
||||
|> List.map (fun c -> {
|
||||
Id = if c.Id.StartsWith "new" then OtherContactId.create ()
|
||||
else OtherContactId.ofString c.Id
|
||||
ContactType = ContactType.parse c.ContactType
|
||||
Name = noneIfBlank c.Name
|
||||
Value = c.Value
|
||||
IsPublic = c.IsPublic
|
||||
})
|
||||
}
|
||||
return! ok next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// DELETE: /api/citizen
|
||||
let delete : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
do! Citizens.deleteById (currentCitizenId ctx)
|
||||
@ -447,7 +475,10 @@ let allEndpoints = [
|
||||
subRoute "/api" [
|
||||
subRoute "/citizen" [
|
||||
GET_HEAD [ routef "/%O" Citizen.get ]
|
||||
PATCH [ route "/confirm" Citizen.confirmToken ]
|
||||
PATCH [
|
||||
route "/account" Citizen.account
|
||||
route "/confirm" Citizen.confirmToken
|
||||
]
|
||||
POST [
|
||||
route "/log-on" Citizen.logOn
|
||||
route "/register" Citizen.register
|
||||
|
Loading…
Reference in New Issue
Block a user