Version 3 #40
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"fake-cli": {
|
||||
"version": "5.22.0",
|
||||
"version": "5.23.0",
|
||||
"commands": [
|
||||
"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",
|
||||
"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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "jobs-jobs-jobs",
|
||||
"version": "2.2.2",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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" /> 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>
|
||||
|
|
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
|
||||
|
||||
/// 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
|
||||
|
|
|
@ -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 _ =
|
||||
|
|
|
@ -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 ]
|
||||
|
|
Loading…
Reference in New Issue
Block a user