WIP on registration
First half is done
This commit is contained in:
parent
97b23cf7d9
commit
5340beb393
|
@ -3,7 +3,7 @@
|
||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"fake-cli": {
|
"fake-cli": {
|
||||||
"version": "5.22.0",
|
"version": "5.23.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"fake"
|
"fake"
|
||||||
]
|
]
|
||||||
|
|
4
src/JobsJobsJobs/App/package-lock.json
generated
4
src/JobsJobsJobs/App/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "jobs-jobs-jobs",
|
"name": "jobs-jobs-jobs",
|
||||||
"version": "2.2.2",
|
"version": "3.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jobs-jobs-jobs",
|
"name": "jobs-jobs-jobs",
|
||||||
"version": "2.2.2",
|
"version": "3.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^6.9.96",
|
"@mdi/js": "^6.9.96",
|
||||||
"@vuelidate/core": "^2.0.0-alpha.24",
|
"@vuelidate/core": "^2.0.0-alpha.24",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "jobs-jobs-jobs",
|
"name": "jobs-jobs-jobs",
|
||||||
"version": "2.2.2",
|
"version": "3.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Citizen,
|
Citizen,
|
||||||
|
CitizenRegistrationForm,
|
||||||
Continent,
|
Continent,
|
||||||
Count,
|
Count,
|
||||||
Instance,
|
Instance,
|
||||||
|
@ -33,12 +34,14 @@ const apiUrl = (url : string) : string => `/api/${url}`
|
||||||
*
|
*
|
||||||
* @param method The method by which the request should be executed
|
* @param method The method by which the request should be executed
|
||||||
* @param user The currently logged-on user
|
* @param user The currently logged-on user
|
||||||
|
* @param body The body of teh request
|
||||||
* @returns RequestInit parameters
|
* @returns RequestInit parameters
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line
|
// 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()
|
const headers = new Headers()
|
||||||
headers.append("Authorization", `Bearer ${user.jwt}`)
|
if (user) headers.append("Authorization", `Bearer ${user.jwt}`)
|
||||||
if (body) {
|
if (body) {
|
||||||
headers.append("Content-Type", "application/json")
|
headers.append("Content-Type", "application/json")
|
||||||
return {
|
return {
|
||||||
|
@ -98,6 +101,15 @@ export default {
|
||||||
/** API functions for citizens */
|
/** API functions for citizens */
|
||||||
citizen: {
|
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
|
* Log a citizen on
|
||||||
*
|
*
|
||||||
|
@ -172,12 +184,12 @@ export default {
|
||||||
* Expire a job listing
|
* Expire a job listing
|
||||||
*
|
*
|
||||||
* @param id The ID of the job listing to be expired
|
* @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
|
* @param user The currently logged-on user
|
||||||
* @returns True if the action was successful, an error string if not
|
* @returns True if the action was successful, an error string if not
|
||||||
*/
|
*/
|
||||||
expire: async (id : string, listing : ListingExpireForm, user : LogOnSuccess) : Promise<boolean | string> =>
|
expire: async (id : string, form : ListingExpireForm, user : LogOnSuccess) : Promise<boolean | string> =>
|
||||||
apiSend(await fetch(apiUrl(`listing/${id}`), reqInit("PATCH", user, listing)), "expiring job listing"),
|
apiSend(await fetch(apiUrl(`listing/${id}`), reqInit("PATCH", user, form)), "expiring job listing"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the job listings posted by the current citizen
|
* Retrieve the job listings posted by the current citizen
|
||||||
|
|
|
@ -50,6 +50,18 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
meta: { title: "Terms of Service" }
|
meta: { title: "Terms of Service" }
|
||||||
},
|
},
|
||||||
// Citizen URLs
|
// 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",
|
path: "/citizen/log-on",
|
||||||
name: "LogOn",
|
name: "LogOn",
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<article>
|
<article>
|
||||||
<h3 class="pb-3">Register</h3>
|
<h3 class="pb-3">Register</h3>
|
||||||
<form class="row g-3">
|
<form class="row g-3">
|
||||||
<div class="col-6">
|
<div class="col-6 col-xl-4">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input class="form-control" type="text" id="firstName" v-model="v$.firstName.$model" placeholder="First Name">
|
<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>
|
<label class="jjj-required" for="firstName">First Name</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6 col-xl-4">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input class="form-control" type="text" id="lastName" v-model="v$.lastName.$model" placeholder="Last Name">
|
<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>
|
<label class="jjj-required" for="firstName">Last Name</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6 col-xl-4">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input class="form-control" type="text" id="displayName" v-model="v$.displayName.$model"
|
<input class="form-control" type="text" id="displayName" v-model="v$.displayName.$model"
|
||||||
placeholder="Display Name">
|
placeholder="Display Name">
|
||||||
|
@ -24,42 +24,50 @@
|
||||||
<div class="form-text"><em>Optional; overrides "FirstName LastName"</em></div>
|
<div class="form-text"><em>Optional; overrides "FirstName LastName"</em></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6 col-xl-4">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input class="form-control" type="email" id="email" v-model="v$.email.$model" placeholder="E-mail Address">
|
<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>
|
<label class="jjj-required" for="email">E-mail Address</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6 col-xl-4">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input class="form-control" type="password" id="password" v-model="v$.password.$model" placeholder="Password"
|
<input class="form-control" type="password" id="password" v-model="v$.password.$model" placeholder="Password"
|
||||||
minlength="8">
|
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>
|
<label class="jjj-required" for="password">Password</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6 col-xl-4">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input class="form-control" type="password" id="confirmPassword" v-model="v$.confirmPassword.$model"
|
<input class="form-control" type="password" id="confirmPassword" v-model="v$.confirmPassword.$model"
|
||||||
placeholder="Confirm Password">
|
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>
|
<label class="jjj-required" for="confirmPassword">Confirm Password</label>
|
||||||
</div>
|
</div>
|
||||||
</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" /> Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, reactive } from "vue"
|
import { computed, reactive } from "vue"
|
||||||
import api, { Citizen, CitizenRegistrationForm, LogOnSuccess, Profile } from "@/api"
|
import { useRouter } from "vue-router"
|
||||||
|
import { mdiContentSaveOutline } from "@mdi/js"
|
||||||
import useVuelidate from "@vuelidate/core"
|
import useVuelidate from "@vuelidate/core"
|
||||||
import { email, minLength, required, sameAs } from "@vuelidate/validators"
|
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 */
|
/** The information required to register a user */
|
||||||
const regForm = reactive(new CitizenRegistrationForm())
|
const regForm = reactive(new CitizenRegistrationForm())
|
||||||
|
@ -77,8 +85,17 @@ const rules = computed(() => ({
|
||||||
/** Initialize form validation */
|
/** Initialize form validation */
|
||||||
const v$ = useVuelidate(rules, regForm, { $lazy: true })
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
15
src/JobsJobsJobs/App/src/views/citizen/Registered.vue
Normal file
15
src/JobsJobsJobs/App/src/views/citizen/Registered.vue
Normal 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>
|
|
@ -7,6 +7,27 @@ open NodaTime
|
||||||
|
|
||||||
// fsharplint:disable FieldNames
|
// 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
|
/// The data required to add or edit a job listing
|
||||||
type ListingForm =
|
type ListingForm =
|
||||||
{ /// The ID of the listing
|
{ /// The ID of the listing
|
||||||
|
|
|
@ -37,9 +37,7 @@ let configureServices (svc : IServiceCollection) =
|
||||||
let _ = svc.AddCors ()
|
let _ = svc.AddCors ()
|
||||||
|
|
||||||
let _ = svc.AddSingleton<Json.ISerializer> (SystemTextJson.Serializer Json.options)
|
let _ = svc.AddSingleton<Json.ISerializer> (SystemTextJson.Serializer Json.options)
|
||||||
|
let cfg = svc.BuildServiceProvider().GetRequiredService<IConfiguration> ()
|
||||||
let svcs = svc.BuildServiceProvider ()
|
|
||||||
let cfg = svcs.GetRequiredService<IConfiguration> ()
|
|
||||||
|
|
||||||
// Set up JWTs for API access
|
// Set up JWTs for API access
|
||||||
let _ =
|
let _ =
|
||||||
|
@ -59,11 +57,9 @@ let configureServices (svc : IServiceCollection) =
|
||||||
let _ = svc.AddAuthorization ()
|
let _ = svc.AddAuthorization ()
|
||||||
let _ = svc.Configure<AuthOptions> (cfg.GetSection "Auth")
|
let _ = svc.Configure<AuthOptions> (cfg.GetSection "Auth")
|
||||||
|
|
||||||
// Set up the Marten data store
|
// Set up the data store
|
||||||
match DataConnection.setUp cfg |> Async.AwaitTask |> Async.RunSynchronously with
|
let _ = DataConnection.setUp cfg |> Async.AwaitTask |> Async.RunSynchronously
|
||||||
| Ok _ -> ()
|
()
|
||||||
| Error msg -> failwith $"Error initializing data store: {msg}"
|
|
||||||
|
|
||||||
|
|
||||||
[<EntryPoint>]
|
[<EntryPoint>]
|
||||||
let main _ =
|
let main _ =
|
||||||
|
|
|
@ -102,6 +102,31 @@ open JobsJobsJobs.Data
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Citizen =
|
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]
|
// 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
|
||||||
|
@ -463,6 +488,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
|
||||||
]
|
]
|
||||||
|
POST [ route "/register" Citizen.register ]
|
||||||
DELETE [ route "" Citizen.delete ]
|
DELETE [ route "" Citizen.delete ]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/continents" Continent.all ]
|
GET_HEAD [ route "/continents" Continent.all ]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user