Implements reset password modal under login modal.
This commit is contained in:
parent
595b0ea086
commit
c0ecf782c8
4 changed files with 129 additions and 27 deletions
|
|
@ -87,10 +87,21 @@ proc getGravatarUrl(email: string, size = 80): string =
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
template `||`(x: untyped): untyped = (if not isNil(x): x else: "")
|
template `||`(x: untyped): untyped = (if not isNil(x): x else: "")
|
||||||
|
|
||||||
|
proc validateCaptcha(recaptchaResp, ip: string) {.async.} =
|
||||||
|
# captcha validation:
|
||||||
|
if config.recaptchaSecretKey.len > 0:
|
||||||
|
var verifyFut = captcha.verify(recaptchaResp, ip)
|
||||||
|
yield verifyFut
|
||||||
|
if verifyFut.failed:
|
||||||
|
raise newForumError(
|
||||||
|
"Invalid recaptcha answer", @[]
|
||||||
|
)
|
||||||
|
|
||||||
proc resetPassword(
|
proc sendResetPassword(
|
||||||
c: TForumData,
|
c: TForumData,
|
||||||
email: string
|
email: string,
|
||||||
|
recaptchaResp: string,
|
||||||
|
userIp: string
|
||||||
) {.async.} =
|
) {.async.} =
|
||||||
# Gather some extra information to determine ident hash.
|
# Gather some extra information to determine ident hash.
|
||||||
let row = db.getRow(
|
let row = db.getRow(
|
||||||
|
|
@ -101,7 +112,9 @@ proc resetPassword(
|
||||||
email, email
|
email, email
|
||||||
)
|
)
|
||||||
if row[0] == "":
|
if row[0] == "":
|
||||||
raise newForumError("Email not found", @["email"])
|
raise newForumError("Email or username not found", @["email"])
|
||||||
|
|
||||||
|
await validateCaptcha(recaptchaResp, userIp)
|
||||||
|
|
||||||
await sendSecureEmail(
|
await sendSecureEmail(
|
||||||
mailer,
|
mailer,
|
||||||
|
|
@ -560,14 +573,7 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp,
|
||||||
if pass.len < 4:
|
if pass.len < 4:
|
||||||
raise newForumError("Please choose a longer password", @["password"])
|
raise newForumError("Please choose a longer password", @["password"])
|
||||||
|
|
||||||
# captcha validation:
|
await validateCaptcha(antibot, userIp)
|
||||||
if config.recaptchaSecretKey.len > 0:
|
|
||||||
var verifyFut = captcha.verify(antibot, userIp)
|
|
||||||
yield verifyFut
|
|
||||||
if verifyFut.failed:
|
|
||||||
raise newForumError(
|
|
||||||
"Invalid recaptcha answer", @[]
|
|
||||||
)
|
|
||||||
|
|
||||||
# perform registration:
|
# perform registration:
|
||||||
var salt = makeSalt()
|
var salt = makeSalt()
|
||||||
|
|
@ -1197,17 +1203,28 @@ routes:
|
||||||
|
|
||||||
post "/sendResetPassword":
|
post "/sendResetPassword":
|
||||||
createTFD()
|
createTFD()
|
||||||
|
|
||||||
|
let formData = request.formData
|
||||||
|
let recaptcha =
|
||||||
|
if "g-recaptcha-response" in formData:
|
||||||
|
formData["g-recaptcha-response"].body
|
||||||
|
else:
|
||||||
|
""
|
||||||
|
|
||||||
if not c.loggedIn():
|
if not c.loggedIn():
|
||||||
|
if not config.isDev:
|
||||||
|
if "g-recaptcha-response" notin formData:
|
||||||
let err = PostError(
|
let err = PostError(
|
||||||
errorFields: @[],
|
errorFields: @[],
|
||||||
message: "Not logged in."
|
message: "Not logged in."
|
||||||
)
|
)
|
||||||
resp Http401, $(%err), "application/json"
|
resp Http401, $(%err), "application/json"
|
||||||
|
|
||||||
let formData = request.formData
|
|
||||||
cond "email" in formData
|
cond "email" in formData
|
||||||
try:
|
try:
|
||||||
await resetPassword(c, formData["email"].body)
|
await sendResetPassword(
|
||||||
|
c, formData["email"].body, recaptcha, request.host
|
||||||
|
)
|
||||||
resp Http200, "{}", "application/json"
|
resp Http200, "{}", "application/json"
|
||||||
except ForumError:
|
except ForumError:
|
||||||
let exc = (ref ForumError)(getCurrentException())
|
let exc = (ref ForumError)(getCurrentException())
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ when defined(js):
|
||||||
text submessage
|
text submessage
|
||||||
|
|
||||||
proc genFormField*(error: Option[PostError], name, label, typ: string,
|
proc genFormField*(error: Option[PostError], name, label, typ: string,
|
||||||
isLast: bool): VNode =
|
isLast: bool, placeholder=""): VNode =
|
||||||
let hasError =
|
let hasError =
|
||||||
not error.isNone and (
|
not error.isNone and (
|
||||||
name in error.get().errorFields or
|
name in error.get().errorFields or
|
||||||
|
|
@ -63,13 +63,14 @@ when defined(js):
|
||||||
tdiv(class=class({"has-error": hasError}, "form-group")):
|
tdiv(class=class({"has-error": hasError}, "form-group")):
|
||||||
label(class="form-label", `for`=name):
|
label(class="form-label", `for`=name):
|
||||||
text label
|
text label
|
||||||
input(class="form-input", `type`=typ, name=name)
|
input(class="form-input", `type`=typ, name=name,
|
||||||
|
placeholder=placeholder)
|
||||||
|
|
||||||
if not error.isNone:
|
if not error.isNone:
|
||||||
let e = error.get()
|
let e = error.get()
|
||||||
if (e.errorFields.len == 1 and e.errorFields[0] == name) or
|
if (e.errorFields.len == 1 and e.errorFields[0] == name) or
|
||||||
(isLast and e.errorFields.len == 0):
|
(isLast and e.errorFields.len == 0):
|
||||||
p(class="form-input-hint"):
|
span(class="form-input-hint"):
|
||||||
text e.message
|
text e.message
|
||||||
|
|
||||||
template postFinished*(onSuccess: untyped): untyped =
|
template postFinished*(onSuccess: untyped): untyped =
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ when defined(js):
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import karax / [kajax, kdom]
|
import karax / [kajax, kdom]
|
||||||
|
|
||||||
import error
|
import error, resetpassword
|
||||||
import karaxutils
|
import karaxutils
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|
@ -15,6 +15,7 @@ when defined(js):
|
||||||
onLogIn: proc ()
|
onLogIn: proc ()
|
||||||
onSignUp: proc ()
|
onSignUp: proc ()
|
||||||
error: Option[PostError]
|
error: Option[PostError]
|
||||||
|
resetPasswordModal: ResetPasswordModal
|
||||||
|
|
||||||
proc onLogInPost(httpStatus: int, response: kstring, state: LoginModal) =
|
proc onLogInPost(httpStatus: int, response: kstring, state: LoginModal) =
|
||||||
postFinished:
|
postFinished:
|
||||||
|
|
@ -40,7 +41,8 @@ when defined(js):
|
||||||
LoginModal(
|
LoginModal(
|
||||||
shown: false,
|
shown: false,
|
||||||
onLogIn: onLogIn,
|
onLogIn: onLogIn,
|
||||||
onSignUp: onSignUp
|
onSignUp: onSignUp,
|
||||||
|
resetPasswordModal: newResetPasswordModal()
|
||||||
)
|
)
|
||||||
|
|
||||||
proc show*(state: LoginModal) =
|
proc show*(state: LoginModal) =
|
||||||
|
|
@ -52,7 +54,7 @@ when defined(js):
|
||||||
onLogInClick(e, n, state)
|
onLogInClick(e, n, state)
|
||||||
|
|
||||||
proc render*(state: LoginModal): VNode =
|
proc render*(state: LoginModal): VNode =
|
||||||
result = buildHtml():
|
result = buildHtml(tdiv()):
|
||||||
tdiv(class=class({"active": state.shown}, "modal modal-sm"),
|
tdiv(class=class({"active": state.shown}, "modal modal-sm"),
|
||||||
id="login-modal"):
|
id="login-modal"):
|
||||||
a(href="", class="modal-overlay", "aria-label"="close",
|
a(href="", class="modal-overlay", "aria-label"="close",
|
||||||
|
|
@ -76,7 +78,8 @@ when defined(js):
|
||||||
"password",
|
"password",
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
a(href="#reset-password-modal"):
|
a(href="", onClick=(e: Event, n: VNode) =>
|
||||||
|
(state.resetPasswordModal.show(); e.preventDefault())):
|
||||||
text "Reset your password"
|
text "Reset your password"
|
||||||
tdiv(class="modal-footer"):
|
tdiv(class="modal-footer"):
|
||||||
button(class=class(
|
button(class=class(
|
||||||
|
|
@ -89,3 +92,5 @@ when defined(js):
|
||||||
onClick=(ev: Event, n: VNode) =>
|
onClick=(ev: Event, n: VNode) =>
|
||||||
(state.onSignUp(); state.shown = false)):
|
(state.onSignUp(); state.shown = false)):
|
||||||
text "Create account"
|
text "Create account"
|
||||||
|
|
||||||
|
render(state.resetPasswordModal)
|
||||||
|
|
@ -65,3 +65,82 @@ when defined(js):
|
||||||
onClick=(ev: Event, n: VNode) =>
|
onClick=(ev: Event, n: VNode) =>
|
||||||
(onSetClick(ev, n, state))):
|
(onSetClick(ev, n, state))):
|
||||||
text "Set password"
|
text "Set password"
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
ResetPasswordModal* = ref object
|
||||||
|
shown: bool
|
||||||
|
loading: bool
|
||||||
|
error: Option[PostError]
|
||||||
|
sent: bool
|
||||||
|
|
||||||
|
proc onPost(httpStatus: int, response: kstring, state: ResetPasswordModal) =
|
||||||
|
postFinished:
|
||||||
|
state.sent = true
|
||||||
|
|
||||||
|
proc onClick(ev: Event, n: VNode, state: ResetPasswordModal) =
|
||||||
|
state.loading = true
|
||||||
|
state.error = none[PostError]()
|
||||||
|
|
||||||
|
let uri = makeUri("sendResetPassword")
|
||||||
|
let form = dom.document.getElementById("resetpassword-form")
|
||||||
|
# TODO: This is a hack, karax should support this.
|
||||||
|
let formData = newFormData(form)
|
||||||
|
ajaxPost(uri, @[], cast[cstring](formData),
|
||||||
|
(s: int, r: kstring) => onPost(s, r, state))
|
||||||
|
|
||||||
|
proc onClose(ev: Event, n: VNode, state: ResetPasswordModal) =
|
||||||
|
state.shown = false
|
||||||
|
ev.preventDefault()
|
||||||
|
|
||||||
|
proc newResetPasswordModal*(): ResetPasswordModal =
|
||||||
|
ResetPasswordModal(
|
||||||
|
shown: false
|
||||||
|
)
|
||||||
|
|
||||||
|
proc show*(state: ResetPasswordModal) =
|
||||||
|
state.shown = true
|
||||||
|
|
||||||
|
proc onKeyDown(e: Event, n: VNode, state: ResetPasswordModal) =
|
||||||
|
let event = cast[KeyboardEvent](e)
|
||||||
|
if event.key == "Enter":
|
||||||
|
onClick(e, n, state)
|
||||||
|
|
||||||
|
proc render*(state: ResetPasswordModal): VNode =
|
||||||
|
result = buildHtml():
|
||||||
|
tdiv(class=class({"active": state.shown}, "modal"),
|
||||||
|
id="resetpassword-modal"):
|
||||||
|
a(href="", class="modal-overlay", "aria-label"="close",
|
||||||
|
onClick=(ev: Event, n: VNode) => onClose(ev, n, state))
|
||||||
|
tdiv(class="modal-container"):
|
||||||
|
tdiv(class="modal-header"):
|
||||||
|
a(href="", class="btn btn-clear float-right",
|
||||||
|
"aria-label"="close",
|
||||||
|
onClick=(ev: Event, n: VNode) => onClose(ev, n, state))
|
||||||
|
tdiv(class="modal-title h5"):
|
||||||
|
text "Reset your password"
|
||||||
|
tdiv(class="modal-body"):
|
||||||
|
tdiv(class="content"):
|
||||||
|
form(id="resetpassword-form",
|
||||||
|
onKeyDown=(ev: Event, n: VNode) => onKeyDown(ev, n, state)):
|
||||||
|
genFormField(
|
||||||
|
state.error,
|
||||||
|
"email",
|
||||||
|
"Enter your email or username and we will send you a " &
|
||||||
|
"password reset email.",
|
||||||
|
"text",
|
||||||
|
true,
|
||||||
|
placeholder="Username or email"
|
||||||
|
)
|
||||||
|
tdiv(class="modal-footer"):
|
||||||
|
if state.sent:
|
||||||
|
span(class="text-success"):
|
||||||
|
italic(class="fas fa-check-circle")
|
||||||
|
text " Sent"
|
||||||
|
else:
|
||||||
|
button(class=class(
|
||||||
|
{"loading": state.loading},
|
||||||
|
"btn btn-primary"
|
||||||
|
),
|
||||||
|
onClick=(ev: Event, n: VNode) => onClick(ev, n, state)):
|
||||||
|
text "Reset password"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue