Finish password reset

This commit is contained in:
Daniel J. Summers 2023-01-19 16:59:43 -05:00
parent a79fb46c99
commit adc976081e
4 changed files with 69 additions and 9 deletions

View File

@ -68,10 +68,7 @@ let main args =
let _ = app.UseAuthorization () let _ = app.UseAuthorization ()
let _ = app.UseSession () let _ = app.UseSession ()
let _ = app.UseGiraffeErrorHandler Handlers.Error.unexpectedError let _ = app.UseGiraffeErrorHandler Handlers.Error.unexpectedError
let _ = app.UseEndpoints ( let _ = app.UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.allEndpoints |> ignore)
fun e ->
e.MapGiraffeEndpoints Handlers.allEndpoints
e.MapFallbackToFile "index.html" |> ignore)
app.Run () app.Run ()

View File

@ -63,7 +63,6 @@ module Helpers =
open Microsoft.AspNetCore.Antiforgery open Microsoft.AspNetCore.Antiforgery
open Microsoft.Extensions.Configuration open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Options
/// Get the NodaTime clock from the request context /// Get the NodaTime clock from the request context
let now (ctx : HttpContext) = ctx.GetService<IClock>().GetCurrentInstant () let now (ctx : HttpContext) = ctx.GetService<IClock>().GetCurrentInstant ()
@ -437,11 +436,37 @@ module Citizen =
let resetPassword token : HttpHandler = fun next ctx -> task { let resetPassword token : HttpHandler = fun next ctx -> task {
match! Citizens.trySecurityByToken token with match! Citizens.trySecurityByToken token with
| Some security -> | Some security ->
// TODO: create form and page return!
return! Home.home |> render "TODO" next ctx Citizen.resetPassword { Id = CitizenId.toString security.Id; Token = token; Password = "" } (csrf ctx)
|> render "Reset Password" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// POST: /citizen/reset-password
let doResetPassword : HttpHandler = validateCsrf >=> fun next ctx -> task {
let! form = ctx.BindFormAsync<ResetPasswordForm> ()
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 // POST: /citizen/save-account
let saveAccount : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { let saveAccount : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
let! theForm = ctx.BindFormAsync<AccountProfileForm> () let! theForm = ctx.BindFormAsync<AccountProfileForm> ()
@ -870,6 +895,7 @@ let allEndpoints = [
route "/forgot-password" Citizen.doForgotPassword route "/forgot-password" Citizen.doForgotPassword
route "/log-on" Citizen.doLogOn route "/log-on" Citizen.doLogOn
route "/register" Citizen.doRegistration route "/register" Citizen.doRegistration
route "/reset-password" Citizen.doResetPassword
route "/save-account" Citizen.saveAccount route "/save-account" Citizen.saveAccount
] ]
] ]

View File

@ -309,3 +309,17 @@ module RegisterViewModel =
Question2Index = 0 Question2Index = 0
Question2Answer = "" Question2Answer = ""
} }
/// The form for a user resetting their password
[<CLIMutable; NoComparison; NoEquality>]
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
}

View File

@ -255,7 +255,7 @@ let forgotPassword csrf =
div [ _class "col-12 col-md-6 offset-md-3" ] [ div [ _class "col-12 col-md-6 offset-md-3" ] [
textBox [ _type "email"; _autofocus ] (nameof m.Email) m.Email "E-mail Address" true 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 /// The page displayed after a forgotten / reset request has been processed
let forgotPasswordSent (m : ForgotPasswordForm) = let forgotPasswordSent (m : ForgotPasswordForm) =
pageWithTitle "Reset Request Processed" [ 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." 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)"
]
]