diff --git a/src/JobsJobsJobs/Server/App.fs b/src/JobsJobsJobs/Server/App.fs index e6236f4..a1d5547 100644 --- a/src/JobsJobsJobs/Server/App.fs +++ b/src/JobsJobsJobs/Server/App.fs @@ -68,10 +68,7 @@ let main args = let _ = app.UseAuthorization () let _ = app.UseSession () let _ = app.UseGiraffeErrorHandler Handlers.Error.unexpectedError - let _ = app.UseEndpoints ( - fun e -> - e.MapGiraffeEndpoints Handlers.allEndpoints - e.MapFallbackToFile "index.html" |> ignore) + let _ = app.UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.allEndpoints |> ignore) app.Run () diff --git a/src/JobsJobsJobs/Server/Handlers.fs b/src/JobsJobsJobs/Server/Handlers.fs index df02c96..74330f7 100644 --- a/src/JobsJobsJobs/Server/Handlers.fs +++ b/src/JobsJobsJobs/Server/Handlers.fs @@ -63,7 +63,6 @@ module Helpers = open Microsoft.AspNetCore.Antiforgery open Microsoft.Extensions.Configuration open Microsoft.Extensions.DependencyInjection - open Microsoft.Extensions.Options /// Get the NodaTime clock from the request context let now (ctx : HttpContext) = ctx.GetService().GetCurrentInstant () @@ -437,11 +436,37 @@ module Citizen = let resetPassword token : HttpHandler = fun next ctx -> task { match! Citizens.trySecurityByToken token with | Some security -> - // TODO: create form and page - return! Home.home |> render "TODO" next ctx + return! + Citizen.resetPassword { Id = CitizenId.toString security.Id; Token = token; Password = "" } (csrf ctx) + |> render "Reset Password" next ctx | None -> return! Error.notFound next ctx } + // POST: /citizen/reset-password + let doResetPassword : HttpHandler = validateCsrf >=> fun next ctx -> task { + let! form = ctx.BindFormAsync () + let errors = [ + if form.Id = "" then "Request invalid; please return to the link in your e-mail and try again" + if form.Token = "" then "Request invalid; please return to the link in your e-mail and try again" + if form.Password.Length < 8 then "Password too short" + ] + if List.isEmpty errors then + match! Citizens.trySecurityByToken form.Token with + | Some security when security.Id = CitizenId.ofString form.Id -> + match! Citizens.findById security.Id with + | Some citizen -> + do! Citizens.saveSecurityInfo { security with Token = None; TokenUsage = None; TokenExpires = None } + do! Citizens.save { citizen with PasswordHash = Auth.Passwords.hash citizen form.Password } + do! addSuccess "Password reset successfully; you may log on with your new credentials" ctx + return! redirectToGet "/citizen/log-on" next ctx + | None -> return! Error.notFound next ctx + | Some _ + | None -> return! Error.notFound next ctx + else + do! addErrors errors ctx + return! Citizen.resetPassword form (csrf ctx) |> render "Reset Password" next ctx + } + // POST: /citizen/save-account let saveAccount : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { let! theForm = ctx.BindFormAsync () @@ -870,6 +895,7 @@ let allEndpoints = [ route "/forgot-password" Citizen.doForgotPassword route "/log-on" Citizen.doLogOn route "/register" Citizen.doRegistration + route "/reset-password" Citizen.doResetPassword route "/save-account" Citizen.saveAccount ] ] diff --git a/src/JobsJobsJobs/Server/ViewModels.fs b/src/JobsJobsJobs/Server/ViewModels.fs index e94c556..6b6f415 100644 --- a/src/JobsJobsJobs/Server/ViewModels.fs +++ b/src/JobsJobsJobs/Server/ViewModels.fs @@ -309,3 +309,17 @@ module RegisterViewModel = Question2Index = 0 Question2Answer = "" } + + +/// The form for a user resetting their password +[] +type ResetPasswordForm = + { /// The ID of the citizen whose password is being reset + Id : string + + /// The verification token for the password reset + Token : string + + /// The new password for the account + Password : string + } diff --git a/src/JobsJobsJobs/Server/Views/Citizen.fs b/src/JobsJobsJobs/Server/Views/Citizen.fs index 124be54..a96e042 100644 --- a/src/JobsJobsJobs/Server/Views/Citizen.fs +++ b/src/JobsJobsJobs/Server/Views/Citizen.fs @@ -255,7 +255,7 @@ let forgotPassword csrf = div [ _class "col-12 col-md-6 offset-md-3" ] [ textBox [ _type "email"; _autofocus ] (nameof m.Email) m.Email "E-mail Address" true ] - div [ _class "col-12" ] [ submitButton "login" "Send Reset Link" ] + div [ _class "col-12" ] [ submitButton "send-lock-outline" "Send Reset Link" ] ] ] @@ -263,7 +263,10 @@ let forgotPassword csrf = /// The page displayed after a forgotten / reset request has been processed let forgotPasswordSent (m : ForgotPasswordForm) = pageWithTitle "Reset Request Processed" [ - p [] [ txt "The reset link request has been processed; check your e-mail for further instructions." ] + p [] [ + txt "The reset link request has been processed. If the e-mail address matched an account, further " + txt "instructions were sent to that address." + ] ] @@ -370,3 +373,23 @@ let resetCanceled wasCanceled = else txt "There was no active password reset request found; it may have already expired." ] ] + + +/// The password reset page +let resetPassword (m : ResetPasswordForm) csrf = + pageWithTitle "Reset Password" [ + p [] [ txt "Enter your new password in the fields below" ] + form [ _class "row g-3"; _method "POST"; _action "/citizen/reset-password" ] [ + antiForgery csrf + input [ _type "hidden"; _name (nameof m.Id); _value m.Id ] + input [ _type "hidden"; _name (nameof m.Token); _value m.Token ] + div [ _class "col-12 col-md-6 col-xl-4 offset-xl-2" ] [ + textBox [ _type "password"; _minlength "8"; _autofocus ] (nameof m.Password) "" "New Password" true + ] + div [ _class "col-12 col-md-6 col-xl-4" ] [ + textBox [ _type "password"; _minlength "8" ] "ConfirmPassword" "" "Confirm New Password" true + ] + div [ _class "col-12" ] [ submitButton "lock-reset" "Reset Password" ] + jsOnLoad $"jjj.citizen.validatePasswords('{nameof m.Password}', 'ConfirmPassword', true)" + ] + ]