Version 3 #40

Merged
danieljsummers merged 67 commits from version-2-3 into main 2023-02-02 23:47:28 +00:00
2 changed files with 108 additions and 15 deletions
Showing only changes of commit 663c37b642 - Show all commits

View File

@ -99,6 +99,14 @@ module Helpers =
// -- NEW -- // -- NEW --
/// Add a message to the response
let sendMessage (msg : string) : HttpHandler =
setHttpHeader "X-Message" msg
/// Add an error message to the response
let sendError (msg : string) : HttpHandler =
sendMessage $"ERROR|||{msg}"
/// Render a page-level view /// Render a page-level view
let render pageTitle content : HttpHandler = fun _ ctx -> task { let render pageTitle content : HttpHandler = fun _ ctx -> task {
let renderFunc = if ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh then Layout.partial else Layout.full let renderFunc = if ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh then Layout.partial else Layout.full
@ -138,36 +146,67 @@ module Citizen =
seq { seq {
for idx in 0..4 do for idx in 0..4 do
let section = qs.GetSection(string idx) let section = qs.GetSection(string idx)
yield section["Question"], section["Answer"] yield section["Question"], (section["Answer"].ToLowerInvariant ())
} }
|> Array.ofSeq |> Array.ofSeq
challenges <- Some qAndA challenges <- Some qAndA
qAndA qAndA
// GET: /citizen/confirm/[token]
let confirm token = fun next ctx -> task {
let! isConfirmed = Citizens.confirmAccount token
return! render "Account Confirmation" (Citizen.confirmAccount isConfirmed) next ctx
}
// GET: /citizen/deny/[token]
let deny token = fun next ctx -> task {
let! wasDeleted = Citizens.denyAccount token
return! render "Account Deletion" (Citizen.denyAccount wasDeleted) next ctx
}
// GET: /citizen/log-on // GET: /citizen/log-on
let logOn : HttpHandler = let logOn : HttpHandler =
render "Log On" (Citizen.logOn { ErrorMessage = None; Email = ""; Password = "" }) render "Log On" (Citizen.logOn { ErrorMessage = None; Email = ""; Password = "" })
// GET: /citizen/register // GET: /citizen/register
let register : HttpHandler = fun next ctx -> task { let register : HttpHandler = fun next ctx ->
// Get two different indexes for NA-knowledge challenge questions // Get two different indexes for NA-knowledge challenge questions
let q1Index = System.Random.Shared.Next(0, 5) let q1Index = System.Random.Shared.Next(0, 5)
let mutable q2Index = System.Random.Shared.Next(0, 5) let mutable q2Index = System.Random.Shared.Next(0, 5)
while q1Index = q2Index do while q1Index = q2Index do
q2Index <- System.Random.Shared.Next(0, 5) q2Index <- System.Random.Shared.Next(0, 5)
let qAndA = Support.questions ctx let qAndA = Support.questions ctx
return! render "Register" render "Register"
(Citizen.register (fst qAndA[q1Index]) (fst qAndA[q2Index]) (Citizen.register (fst qAndA[q1Index]) (fst qAndA[q2Index])
{ RegisterViewModel.empty with Question1Index = q1Index; Question2Index = q2Index }) next ctx { RegisterViewModel.empty with Question1Index = q1Index; Question2Index = q2Index }) next ctx
}
// POST: /citizen/register // POST: /citizen/register
let doRegistration : HttpHandler = fun next ctx -> task { let doRegistration : HttpHandler = fun next ctx -> task {
let! form = ctx.BindFormAsync<RegisterViewModel> () let! form = ctx.BindFormAsync<RegisterViewModel> ()
// FIXME: stopped here; add validation let qAndA = Support.questions ctx
if form.Password.Length < 8 then let mutable badForm = false
return! RequestErrors.BAD_REQUEST "Password out of range" next ctx let errors = [
else if form.FirstName.Length < 1 then "First name is required"
if form.LastName.Length < 1 then "Last name is required"
if form.Email.Length < 1 then "E-mail address is required"
if form.Password.Length < 8 then "Password is too short"
if form.Question1Index < 0 || form.Question1Index > 4
|| form.Question2Index < 0 || form.Question2Index > 4
|| form.Question1Index = form.Question2Index then
badForm <- true
else if (snd qAndA[form.Question1Index]) <> (form.Question1Answer.Trim().ToLowerInvariant ())
|| (snd qAndA[form.Question2Index]) <> (form.Question2Answer.Trim().ToLowerInvariant ()) then
"Question answers are incorrect"
]
let refreshPage () =
render "Register"
(Citizen.register (fst qAndA[form.Question1Index]) (fst qAndA[form.Question2Index])
{ form with Password = "" })
if badForm then
let handle = sendError "The form posted was invalid; please complete it again" >=> register
return! handle next ctx
else if List.isEmpty errors then
let now = now ctx let now = now ctx
let noPass = let noPass =
{ Citizen.empty with { Citizen.empty with
@ -191,12 +230,17 @@ module Citizen =
let! success = Citizens.register citizen security let! success = Citizens.register citizen security
if success then if success then
let! emailResponse = Email.sendAccountConfirmation citizen security let! emailResponse = Email.sendAccountConfirmation citizen security
let logFac = logger ctx let logFac = logger ctx
let log = logFac.CreateLogger "JobsJobsJobs.Api.Handlers.Citizen" let log = logFac.CreateLogger "JobsJobsJobs.Handlers.Citizen"
log.LogInformation $"Confirmation e-mail for {citizen.Email} received {emailResponse}" log.LogInformation $"Confirmation e-mail for {citizen.Email} received {emailResponse}"
return! ok next ctx return! render "Registration Successful" Citizen.registered next ctx
else else
return! RequestErrors.CONFLICT "" next ctx return! (sendError "There is already an account registered to the e-mail address provided"
>=> refreshPage ()) next ctx
else
let errMsg = String.Join ("</li><li>", errors)
return! (sendError $"Please correct the following errors:<ul><li>{errMsg}</li></ul>" >=> refreshPage ())
next ctx
} }
/// Handlers for /api/citizen routes /// Handlers for /api/citizen routes
@ -596,8 +640,10 @@ let allEndpoints = [
GET_HEAD [ route "/" Home.home ] GET_HEAD [ route "/" Home.home ]
subRoute "/citizen" [ subRoute "/citizen" [
GET_HEAD [ GET_HEAD [
route "/log-on" Citizen.logOn routef "/confirm/%s" Citizen.confirm
route "/register" Citizen.register routef "/deny/%s" Citizen.deny
route "/log-on" Citizen.logOn
route "/register" Citizen.register
] ]
POST [ route "/register" Citizen.doRegistration ] POST [ route "/register" Citizen.doRegistration ]
] ]

View File

@ -6,6 +6,34 @@ open Giraffe.ViewEngine
open Giraffe.ViewEngine.Htmx open Giraffe.ViewEngine.Htmx
open JobsJobsJobs.ViewModels open JobsJobsJobs.ViewModels
/// The account confirmation page
let confirmAccount isConfirmed =
article [] [
h3 [ _class "pb-3" ] [ rawText "Account Confirmation" ]
p [] [
if isConfirmed then
rawText "Your account was confirmed successfully! You may "
a [ _href "/citizen/log-on" ] [ rawText "log on here" ]; rawText "."
else
rawText "The confirmation token did not match any pending accounts. Confirmation tokens are only valid "
rawText "for 3 days; if the token expired, you will need to re-register, which "
a [ _href "/citizen/register" ] [ rawText "you can do here" ]; rawText "."
]
]
/// The account denial page
let denyAccount wasDeleted =
article [] [
h3 [ _class "pb-3" ] [ rawText "Account Deletion" ]
p [] [
if wasDeleted then
rawText "The account was deleted successfully; sorry for the trouble."
else
rawText "The confirmation token did not match any pending accounts; if this was an inadvertently "
rawText "created account, it has likely already been deleted."
]
]
/// The log on page /// The log on page
let logOn (m : LogOnViewModel) = let logOn (m : LogOnViewModel) =
article [] [ article [] [
@ -153,3 +181,22 @@ let register q1 q2 (m : RegisterViewModel) =
] ]
] ]
] ]
/// The confirmation page for user registration
let registered =
article [] [
h3 [ _class "pb-3" ] [ rawText "Registration Successful" ]
p [] [
rawText "You have been successfully registered with Jobs, Jobs, Jobs. Check your e-mail for a confirmation "
rawText "link; it will be valid for the next 72 hours (3 days). Once you confirm your account, you will be "
rawText "able to log on using the e-mail address and password you provided."
]
p [] [
rawText "If the account is not confirmed within the 72-hour window, it will be deleted, and you will need "
rawText "to register again."
]
p [] [
rawText "If you encounter issues, feel free to reach out to @danieljsummers on No Agenda Social for "
rawText "assistance."
]
]