WIP on registration

First half is done
This commit is contained in:
Daniel J. Summers 2022-08-28 18:09:05 -04:00
parent 97b23cf7d9
commit 5340beb393
10 changed files with 137 additions and 38 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"fake-cli": {
"version": "5.22.0",
"version": "5.23.0",
"commands": [
"fake"
]

View File

@ -1,12 +1,12 @@
{
"name": "jobs-jobs-jobs",
"version": "2.2.2",
"version": "3.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "jobs-jobs-jobs",
"version": "2.2.2",
"version": "3.0.0",
"dependencies": {
"@mdi/js": "^6.9.96",
"@vuelidate/core": "^2.0.0-alpha.24",

View File

@ -1,6 +1,6 @@
{
"name": "jobs-jobs-jobs",
"version": "2.2.2",
"version": "3.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",

View File

@ -1,5 +1,6 @@
import {
Citizen,
CitizenRegistrationForm,
Continent,
Count,
Instance,
@ -33,12 +34,14 @@ const apiUrl = (url : string) : string => `/api/${url}`
*
* @param method The method by which the request should be executed
* @param user The currently logged-on user
* @param body The body of teh request
* @returns RequestInit parameters
*/
// eslint-disable-next-line
const reqInit = (method : string, user : LogOnSuccess, body : any | undefined = undefined) : RequestInit => {
const reqInit = (method : string, user : LogOnSuccess | undefined, body : any | undefined = undefined)
: RequestInit => {
const headers = new Headers()
headers.append("Authorization", `Bearer ${user.jwt}`)
if (user) headers.append("Authorization", `Bearer ${user.jwt}`)
if (body) {
headers.append("Content-Type", "application/json")
return {
@ -98,6 +101,15 @@ export default {
/** API functions for citizens */
citizen: {
/**
* Register a citizen
*
* @param form The registration details for the citizen
* @returns True if the registration was successful, an error message if it was not
*/
register: async (form : CitizenRegistrationForm) : Promise<boolean | string> =>
apiSend(await fetch(apiUrl("citizen/register"), reqInit("POST", undefined, form)), "registering citizen"),
/**
* Log a citizen on
*
@ -172,12 +184,12 @@ export default {
* Expire a job listing
*
* @param id The ID of the job listing to be expired
* @param form The information needed to expire the form
* @param form The information needed to expire the listing
* @param user The currently logged-on user
* @returns True if the action was successful, an error string if not
*/
expire: async (id : string, listing : ListingExpireForm, user : LogOnSuccess) : Promise<boolean | string> =>
apiSend(await fetch(apiUrl(`listing/${id}`), reqInit("PATCH", user, listing)), "expiring job listing"),
expire: async (id : string, form : ListingExpireForm, user : LogOnSuccess) : Promise<boolean | string> =>
apiSend(await fetch(apiUrl(`listing/${id}`), reqInit("PATCH", user, form)), "expiring job listing"),
/**
* Retrieve the job listings posted by the current citizen

View File

@ -50,6 +50,18 @@ const routes: Array<RouteRecordRaw> = [
meta: { title: "Terms of Service" }
},
// Citizen URLs
{
path: "/citizen/register",
name: "CitizenRegistration",
component: () => import(/* webpackChunkName: "register" */ "../views/citizen/Register.vue"),
meta: { title: "Register" }
},
{
path: "/citizen/registered",
name: "CitizenRegistered",
component: () => import(/* webpackChunkName: "register" */ "../views/citizen/Registered.vue"),
meta: { title: "Registration Successful" }
},
{
path: "/citizen/log-on",
name: "LogOn",

View File

@ -1,22 +1,22 @@
<template>
<template>
<article>
<h3 class="pb-3">Register</h3>
<form class="row g-3">
<div class="col-6">
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="text" id="firstName" v-model="v$.firstName.$model" placeholder="First Name">
<div id="firstNameFeedback" class="invalid-feedback">Please enter your first name</div>
<div v-if="v$.firstName.$error" class="text-danger">Please enter your first name</div>
<label class="jjj-required" for="firstName">First Name</label>
</div>
</div>
<div class="col-6">
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="text" id="lastName" v-model="v$.lastName.$model" placeholder="Last Name">
<div id="lastNameFeedback" class="invalid-feedback">Please enter your last name</div>
<div v-if="v$.lastName.$error" class="text-danger">Please enter your last name</div>
<label class="jjj-required" for="firstName">Last Name</label>
</div>
</div>
<div class="col-6">
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="text" id="displayName" v-model="v$.displayName.$model"
placeholder="Display Name">
@ -24,42 +24,50 @@
<div class="form-text"><em>Optional; overrides "FirstName LastName"</em></div>
</div>
</div>
<div class="col-6">
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="email" id="email" v-model="v$.email.$model" placeholder="E-mail Address">
<div id="emailFeedback" class="invalid-feedback">Please enter a valid e-mail address</div>
<div v-if="v$.email.$error" class="text-danger">Please enter a valid e-mail address</div>
<label class="jjj-required" for="email">E-mail Address</label>
</div>
</div>
<div class="col-6">
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="password" id="password" v-model="v$.password.$model" placeholder="Password"
minlength="8">
<div id="passwordFeedback" class="invalid-feedback">Please enter a password at least 8 characters long</div>
<div v-if="v$.password.$error" class="text-danger">Please enter a password at least 8 characters long</div>
<label class="jjj-required" for="password">Password</label>
</div>
</div>
<div class="col-6">
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="password" id="confirmPassword" v-model="v$.confirmPassword.$model"
placeholder="Confirm Password">
<div id="confirmPasswordFeedback" class="invalid-feedback">The passwords do not match</div>
<div v-if="v$.confirmPassword.$error" class="text-danger">The passwords do not match</div>
<label class="jjj-required" for="confirmPassword">Confirm Password</label>
</div>
</div>
<div class="col-12">
<p class="text-danger" v-if="v$.$error">Please correct the errors above</p>
<button class="btn btn-primary" @click.prevent="saveProfile">
<icon :icon="mdiContentSaveOutline" />&nbsp; Save
</button>
</div>
</form>
</article>
</template>
<script setup lang="ts">
import { computed, ref, reactive } from "vue"
import api, { Citizen, CitizenRegistrationForm, LogOnSuccess, Profile } from "@/api"
import { computed, reactive } from "vue"
import { useRouter } from "vue-router"
import { mdiContentSaveOutline } from "@mdi/js"
import useVuelidate from "@vuelidate/core"
import { email, minLength, required, sameAs } from "@vuelidate/validators"
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
import { useStore } from "@/store"
const store = useStore()
import api, { CitizenRegistrationForm } from "@/api"
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
const router = useRouter()
/** The information required to register a user */
const regForm = reactive(new CitizenRegistrationForm())
@ -77,8 +85,17 @@ const rules = computed(() => ({
/** Initialize form validation */
const v$ = useVuelidate(rules, regForm, { $lazy: true })
/** Register the citizen */
const saveProfile = async () => {
v$.value.$touch()
if (v$.value.$error) return
const registerResult = await api.citizen.register(regForm)
if (typeof registerResult === "string") {
toastError(registerResult, "registering")
} else {
toastSuccess("Registered Successfully")
v$.value.$reset()
await router.push("/citizen/registered")
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
<template>
<article>
<h3 class="pb-3">Registration Successful</h3>
<p>
You have been successfully registered with Jobs, Jobs, Jobs. Check your e-mail for a confirmation link; it will
be valid for the next 72 hours (3 days). Once you confirm your account, you will be able to log on using the
e-mail address and password you provided.
</p>
<p>
If the account is not confirmed within the 72-hour window, it will be deleted, and you will need to register
again.
</p>
<p>If you encounter issues, feel free to reach out to @danieljsummers on No Agenda Social for assistance.</p>
</article>
</template>

View File

@ -7,6 +7,27 @@ open NodaTime
// fsharplint:disable FieldNames
/// The data required to register a new citizen (user)
type CitizenRegistrationForm =
{ /// The first name of the new citizen
FirstName : string
/// The last name of the new citizen
LastName : string
/// The display name for the new citizen
DisplayName : string
/// The citizen's e-mail address
Email : string
/// The citizen's password
Password : string
/// Confirmation of the citizen's password
ConfirmPassword : string
}
/// The data required to add or edit a job listing
type ListingForm =
{ /// The ID of the listing

View File

@ -37,9 +37,7 @@ let configureServices (svc : IServiceCollection) =
let _ = svc.AddCors ()
let _ = svc.AddSingleton<Json.ISerializer> (SystemTextJson.Serializer Json.options)
let svcs = svc.BuildServiceProvider ()
let cfg = svcs.GetRequiredService<IConfiguration> ()
let cfg = svc.BuildServiceProvider().GetRequiredService<IConfiguration> ()
// Set up JWTs for API access
let _ =
@ -59,11 +57,9 @@ let configureServices (svc : IServiceCollection) =
let _ = svc.AddAuthorization ()
let _ = svc.Configure<AuthOptions> (cfg.GetSection "Auth")
// Set up the Marten data store
match DataConnection.setUp cfg |> Async.AwaitTask |> Async.RunSynchronously with
| Ok _ -> ()
| Error msg -> failwith $"Error initializing data store: {msg}"
// Set up the data store
let _ = DataConnection.setUp cfg |> Async.AwaitTask |> Async.RunSynchronously
()
[<EntryPoint>]
let main _ =

View File

@ -101,7 +101,32 @@ open JobsJobsJobs.Data
/// Handlers for /api/citizen routes
[<RequireQualifiedAccess>]
module Citizen =
open Microsoft.AspNetCore.Identity
// POST: /api/citizen/register
let register : HttpHandler = fun next ctx -> task {
let! form = ctx.BindJsonAsync<CitizenRegistrationForm> ()
if form.Password.Length < 8 || form.ConfirmPassword.Length < 8 || form.Password <> form.ConfirmPassword then
return! RequestErrors.BAD_REQUEST "Password out of range" next ctx
else
let now = now ctx
let noPass =
{ Citizen.empty with
Id = CitizenId.create ()
Email = form.Email
FirstName = form.FirstName
LastName = form.LastName
DisplayName = noneIfEmpty form.DisplayName
JoinedOn = now
LastSeenOn = now
}
let citizen = { noPass with PasswordHash = PasswordHasher().HashPassword (noPass, form.Password) }
do! Citizens.save citizen
// TODO: generate auth code and e-mail confirmation
return! ok 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
@ -463,6 +488,7 @@ let allEndpoints = [
routef "/log-on/%s/%s" Citizen.logOn
routef "/%O" Citizen.get
]
POST [ route "/register" Citizen.register ]
DELETE [ route "" Citizen.delete ]
]
GET_HEAD [ route "/continents" Continent.all ]