Implements password resets.
This commit is contained in:
parent
249031b9a8
commit
b2ed4247df
10 changed files with 184 additions and 76 deletions
|
|
@ -45,7 +45,7 @@ proc makePassword*(password, salt: string, comparingTo = ""): string =
|
||||||
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
|
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
|
||||||
result = hash(getMD5(salt & getMD5(password)), bcryptSalt)
|
result = hash(getMD5(salt & getMD5(password)), bcryptSalt)
|
||||||
|
|
||||||
proc makeIdentHash*(user, password, epoch, secret: string,
|
proc makeIdentHash*(user, password: string, epoch: int64, secret: string,
|
||||||
comparingTo = ""): string =
|
comparingTo = ""): string =
|
||||||
## Creates a hash verifying the identity of a user. Used for password reset
|
## Creates a hash verifying the identity of a user. Used for password reset
|
||||||
## links and email activation links.
|
## links and email activation links.
|
||||||
|
|
@ -53,7 +53,7 @@ proc makeIdentHash*(user, password, epoch, secret: string,
|
||||||
## the link is invalid.
|
## the link is invalid.
|
||||||
## The ``secret`` is the 'salt' field in the ``person`` table.
|
## The ``secret`` is the 'salt' field in the ``person`` table.
|
||||||
when defined(windows):
|
when defined(windows):
|
||||||
result = getMD5(user & password & epoch & secret)
|
result = getMD5(user & password & $epoch & secret)
|
||||||
else:
|
else:
|
||||||
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
|
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
|
||||||
result = hash(user & password & epoch & secret, bcryptSalt)
|
result = hash(user & password & $epoch & secret, bcryptSalt)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import asyncdispatch, smtp, strutils, times, cgi, tables
|
import asyncdispatch, smtp, strutils, times, cgi, tables, logging
|
||||||
|
|
||||||
from jester import Request, makeUri
|
from jester import Request, makeUri
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ proc sendMail(
|
||||||
raise newForumError(msg)
|
raise newForumError(msg)
|
||||||
|
|
||||||
if mailer.config.smtpAddress.len == 0:
|
if mailer.config.smtpAddress.len == 0:
|
||||||
echo("[WARNING] Cannot send mail: no smtp server configured (smtpAddress).")
|
warn("Cannot send mail: no smtp server configured (smtpAddress).")
|
||||||
return
|
return
|
||||||
|
|
||||||
var client = newAsyncSmtp()
|
var client = newAsyncSmtp()
|
||||||
|
|
@ -101,7 +101,7 @@ proc sendSecureEmail*(
|
||||||
kind: SecureEmailKind, req: Request,
|
kind: SecureEmailKind, req: Request,
|
||||||
name, password, email, salt: string
|
name, password, email, salt: string
|
||||||
) {.async.} =
|
) {.async.} =
|
||||||
let epoch = $int(epochTime())
|
let epoch = int(epochTime())
|
||||||
|
|
||||||
let path =
|
let path =
|
||||||
case kind
|
case kind
|
||||||
|
|
@ -114,11 +114,13 @@ proc sendSecureEmail*(
|
||||||
[
|
[
|
||||||
path,
|
path,
|
||||||
encodeUrl(name),
|
encodeUrl(name),
|
||||||
encodeUrl(epoch),
|
encodeUrl($epoch),
|
||||||
encodeUrl(makeIdentHash(name, password, epoch, salt))
|
encodeUrl(makeIdentHash(name, password, epoch, salt))
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
debug(url)
|
||||||
|
|
||||||
let emailSentFut =
|
let emailSentFut =
|
||||||
case kind
|
case kind
|
||||||
of ActivateEmail:
|
of ActivateEmail:
|
||||||
|
|
@ -127,7 +129,7 @@ proc sendSecureEmail*(
|
||||||
sendPassReset(mailer, email, name, url)
|
sendPassReset(mailer, email, name, url)
|
||||||
yield emailSentFut
|
yield emailSentFut
|
||||||
if emailSentFut.failed:
|
if emailSentFut.failed:
|
||||||
echo("[WARNING] Couldn't send email: ", emailSentFut.error.msg)
|
warn("Couldn't send email: ", emailSentFut.error.msg)
|
||||||
if emailSentFut.error of ForumError:
|
if emailSentFut.error of ForumError:
|
||||||
raise emailSentFut.error
|
raise emailSentFut.error
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import
|
||||||
os, strutils, times, md5, strtabs, math, db_sqlite,
|
os, strutils, times, md5, strtabs, math, db_sqlite,
|
||||||
scgi, jester, asyncdispatch, asyncnet, sequtils,
|
scgi, jester, asyncdispatch, asyncnet, sequtils,
|
||||||
parseutils, random, rst, recaptcha, json, re, sugar,
|
parseutils, random, rst, recaptcha, json, re, sugar,
|
||||||
strformat
|
strformat, logging
|
||||||
import cgi except setCookie
|
import cgi except setCookie
|
||||||
import options
|
import options
|
||||||
|
|
||||||
|
|
@ -228,16 +228,21 @@ proc rateLimitCheck(c: TForumData): bool =
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
proc verifyIdentHash(c: TForumData, name, epoch, ident: string): bool =
|
proc verifyIdentHash(
|
||||||
|
c: TForumData, name: string, epoch: int64, ident: string
|
||||||
|
) =
|
||||||
const query =
|
const query =
|
||||||
sql"select password, salt, strftime('%s', lastOnline) from person where name = ?"
|
sql"select password, salt, strftime('%s', lastOnline) from person where name = ?"
|
||||||
var row = getRow(db, query, name)
|
var row = getRow(db, query, name)
|
||||||
if row[0] == "": return false
|
if row[0] == "":
|
||||||
|
raise newForumError("User doesn't exist.", @["nick"])
|
||||||
let newIdent = makeIdentHash(name, row[0], epoch, row[1], ident)
|
let newIdent = makeIdentHash(name, row[0], epoch, row[1], ident)
|
||||||
# Check that the user has not been logged in since this ident hash has been
|
# Check that the user has not been logged in since this ident hash has been
|
||||||
# created. Give the timestamp a certain range to prevent false negatives.
|
# created. Give the timestamp a certain range to prevent false negatives.
|
||||||
if row[2].parseInt > (epoch.parseInt + 60): return false
|
if row[2].parseInt > (epoch + 60*3):
|
||||||
result = newIdent == ident
|
raise newForumError("Link expired")
|
||||||
|
if newIdent != ident:
|
||||||
|
raise newForumError("Invalid ident hash")
|
||||||
|
|
||||||
proc initialise() =
|
proc initialise() =
|
||||||
randomize()
|
randomize()
|
||||||
|
|
@ -247,7 +252,7 @@ proc initialise() =
|
||||||
captcha = initReCaptcha(config.recaptchaSecretKey, config.recaptchaSiteKey)
|
captcha = initReCaptcha(config.recaptchaSecretKey, config.recaptchaSiteKey)
|
||||||
else:
|
else:
|
||||||
doAssert config.isDev, "Recaptcha required for production!"
|
doAssert config.isDev, "Recaptcha required for production!"
|
||||||
echo("[WARNING] No recaptcha secret key specified.")
|
warn("No recaptcha secret key specified.")
|
||||||
|
|
||||||
mailer = newMailer(config)
|
mailer = newMailer(config)
|
||||||
|
|
||||||
|
|
@ -1201,7 +1206,7 @@ routes:
|
||||||
let exc = (ref ForumError)(getCurrentException())
|
let exc = (ref ForumError)(getCurrentException())
|
||||||
resp Http400, $(%exc.data), "application/json"
|
resp Http400, $(%exc.data), "application/json"
|
||||||
|
|
||||||
post "/resetPassword":
|
post "/sendResetPassword":
|
||||||
createTFD()
|
createTFD()
|
||||||
if not c.loggedIn():
|
if not c.loggedIn():
|
||||||
let err = PostError(
|
let err = PostError(
|
||||||
|
|
@ -1219,6 +1224,30 @@ routes:
|
||||||
let exc = (ref ForumError)(getCurrentException())
|
let exc = (ref ForumError)(getCurrentException())
|
||||||
resp Http400, $(%exc.data), "application/json"
|
resp Http400, $(%exc.data), "application/json"
|
||||||
|
|
||||||
|
post "/resetPassword":
|
||||||
|
createTFD()
|
||||||
|
cond(@"nick" != "")
|
||||||
|
cond(@"epoch" != "")
|
||||||
|
cond(@"ident" != "")
|
||||||
|
cond(@"newPassword" != "")
|
||||||
|
let epoch = getInt64(@"epoch", -1)
|
||||||
|
try:
|
||||||
|
verifyIdentHash(c, @"nick", epoch, @"ident")
|
||||||
|
var salt = makeSalt()
|
||||||
|
let password = makePassword(@"newPassword", salt)
|
||||||
|
|
||||||
|
exec(
|
||||||
|
db,
|
||||||
|
sql"""
|
||||||
|
update person set password = ?, salt = ?,
|
||||||
|
lastOnline = DATETIME('now')
|
||||||
|
where name = ?;
|
||||||
|
""",
|
||||||
|
password, salt, @"nick"
|
||||||
|
)
|
||||||
|
resp Http200, "{}", "application/json"
|
||||||
|
except ForumError as exc:
|
||||||
|
resp Http400, $(%exc.data),"application/json"
|
||||||
|
|
||||||
get "/t/@id":
|
get "/t/@id":
|
||||||
cond "id" in request.params
|
cond "id" in request.params
|
||||||
|
|
@ -1276,53 +1305,10 @@ routes:
|
||||||
var epoch: BiggestInt = 0
|
var epoch: BiggestInt = 0
|
||||||
cond(parseBiggestInt(@"epoch", epoch) > 0)
|
cond(parseBiggestInt(@"epoch", epoch) > 0)
|
||||||
var success = false
|
var success = false
|
||||||
if verifyIdentHash(c, @"nick", $epoch, @"ident"):
|
|
||||||
let ban = parseEnum[Rank](db.getValue(sql"select status from person where name = ?", @"nick"))
|
|
||||||
# if ban == EmailUnconfirmed:
|
|
||||||
# success = setStatus(c, @"nick", Moderated, "")
|
|
||||||
|
|
||||||
|
|
||||||
get "/emailResetPassword/?":
|
|
||||||
createTFD()
|
|
||||||
cond(@"nick" != "")
|
|
||||||
cond(@"epoch" != "")
|
|
||||||
cond(@"ident" != "")
|
|
||||||
var epoch: BiggestInt = 0
|
|
||||||
cond(parseBiggestInt(@"epoch", epoch) > 0)
|
|
||||||
if verifyIdentHash(c, @"nick", $epoch, @"ident"):
|
|
||||||
let formBody = input(`type`="hidden", name="nick", value = @"nick") &
|
|
||||||
input(`type`="hidden", name="epoch", value = @"epoch") &
|
|
||||||
input(`type`="hidden", name="ident", value = @"ident") &
|
|
||||||
input(`type`="password", name="password") &
|
|
||||||
"<br/>" &
|
|
||||||
input(`type`="submit", name="submitBtn",
|
|
||||||
value="Change my password")
|
|
||||||
let message = htmlgen.p("Please enter a new password for ",
|
|
||||||
htmlgen.b(@"nick"), ':')
|
|
||||||
let content = htmlgen.form(action=c.req.makeUri("/doemailresetpassword"),
|
|
||||||
`method`="POST", message & formBody)
|
|
||||||
|
|
||||||
# resp genMain(c, content, "Reset password - Nim Forum")
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
# resp genMain(c, "Invalid ident hash", "Error - Nim Forum")
|
|
||||||
|
|
||||||
post "/doemailresetpassword":
|
|
||||||
createTFD()
|
|
||||||
cond(@"nick" != "")
|
|
||||||
cond(@"epoch" != "")
|
|
||||||
cond(@"ident" != "")
|
|
||||||
cond(@"password" != "")
|
|
||||||
var epoch: BiggestInt = 0
|
|
||||||
cond(parseBiggestInt(@"epoch", epoch) > 0)
|
|
||||||
# if verifyIdentHash(c, @"nick", $epoch, @"ident"):
|
# if verifyIdentHash(c, @"nick", $epoch, @"ident"):
|
||||||
# let res = setPassword(c, @"nick", @"password")
|
# let ban = parseEnum[Rank](db.getValue(sql"select status from person where name = ?", @"nick"))
|
||||||
# if res:
|
# # if ban == EmailUnconfirmed:
|
||||||
# resp genMain(c, "Password reset successfully!", "Nim Forum")
|
# # success = setStatus(c, @"nick", Moderated, "")
|
||||||
# else:
|
|
||||||
# resp genMain(c, "Password reset failure", "Nim Forum")
|
|
||||||
# else:
|
|
||||||
# resp genMain(c, "Invalid ident hash", "Nim Forum")
|
|
||||||
|
|
||||||
post "/search/?@page?":
|
post "/search/?@page?":
|
||||||
cond isFTSAvailable
|
cond isFTSAvailable
|
||||||
|
|
@ -1336,7 +1322,7 @@ routes:
|
||||||
elif q[i] == '\'': q[i] = '"'
|
elif q[i] == '\'': q[i] = '"'
|
||||||
c.search = q.replace("\"",""")
|
c.search = q.replace("\"",""")
|
||||||
if @"page".len > 0:
|
if @"page".len > 0:
|
||||||
parseInt(@"page", c.pageNum, 0..1000_000)
|
parseIntSafe(@"page", c.pageNum)
|
||||||
cond(c.pageNum > 0)
|
cond(c.pageNum > 0)
|
||||||
iterator searchResults(): db_sqlite.Row {.closure, tags: [ReadDbEffect].} =
|
iterator searchResults(): db_sqlite.Row {.closure, tags: [ReadDbEffect].} =
|
||||||
const queryFT = "fts.sql".slurp.sql
|
const queryFT = "fts.sql".slurp.sql
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,16 @@ when defined(js):
|
||||||
button(class="btn btn-primary"):
|
button(class="btn btn-primary"):
|
||||||
text "Report issue"
|
text "Report issue"
|
||||||
|
|
||||||
|
proc renderMessage*(message, submessage, icon: string): VNode =
|
||||||
|
result = buildHtml():
|
||||||
|
tdiv(class="empty error"):
|
||||||
|
tdiv(class="empty icon"):
|
||||||
|
italic(class="fas " & icon & " fa-5x")
|
||||||
|
p(class="empty-title h5"):
|
||||||
|
text message
|
||||||
|
p(class="empty-subtitle"):
|
||||||
|
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): VNode =
|
||||||
let hasError =
|
let hasError =
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ include karax/prelude
|
||||||
import jester/patterns
|
import jester/patterns
|
||||||
|
|
||||||
import threadlist, postlist, header, profile, newthread, error, about
|
import threadlist, postlist, header, profile, newthread, error, about
|
||||||
|
import resetpassword
|
||||||
import karaxutils
|
import karaxutils
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|
@ -13,6 +14,7 @@ type
|
||||||
profile: ProfileState
|
profile: ProfileState
|
||||||
newThread: NewThread
|
newThread: NewThread
|
||||||
about: About
|
about: About
|
||||||
|
resetPassword: ResetPassword
|
||||||
|
|
||||||
proc copyLocation(loc: Location): Location =
|
proc copyLocation(loc: Location): Location =
|
||||||
# TODO: It sucks that I had to do this. We need a nice way to deep copy in JS.
|
# TODO: It sucks that I had to do this. We need a nice way to deep copy in JS.
|
||||||
|
|
@ -32,7 +34,8 @@ proc newState(): State =
|
||||||
url: copyLocation(window.location),
|
url: copyLocation(window.location),
|
||||||
profile: newProfileState(),
|
profile: newProfileState(),
|
||||||
newThread: newNewThread(),
|
newThread: newNewThread(),
|
||||||
about: newAbout()
|
about: newAbout(),
|
||||||
|
resetPassword: newResetPassword()
|
||||||
)
|
)
|
||||||
|
|
||||||
var state = newState()
|
var state = newState()
|
||||||
|
|
@ -92,6 +95,38 @@ proc render(): VNode =
|
||||||
r("/about/?@page?",
|
r("/about/?@page?",
|
||||||
(params: Params) => (render(state.about, params["page"]))
|
(params: Params) => (render(state.about, params["page"]))
|
||||||
),
|
),
|
||||||
|
r("/activateEmail/success",
|
||||||
|
(params: Params) => (
|
||||||
|
renderMessage(
|
||||||
|
"Email activated",
|
||||||
|
"You can now create new posts!",
|
||||||
|
"fa-check"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
r("/activateEmail/failure",
|
||||||
|
(params: Params) => (
|
||||||
|
renderMessage(
|
||||||
|
"Email activation failed",
|
||||||
|
"Couldn't verify the supplied ident",
|
||||||
|
"fa-exclamation"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
r("/resetPassword/success",
|
||||||
|
(params: Params) => (
|
||||||
|
renderMessage(
|
||||||
|
"Password changed",
|
||||||
|
"You can now login using your new password!",
|
||||||
|
"fa-check"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
r("/resetPassword",
|
||||||
|
(params: Params) => (
|
||||||
|
render(state.resetPassword)
|
||||||
|
)
|
||||||
|
),
|
||||||
r("/404",
|
r("/404",
|
||||||
(params: Params) => render404()
|
(params: Params) => render404()
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,27 @@
|
||||||
import strutils, options, strformat, parseutils
|
import strutils, options, strformat, parseutils
|
||||||
|
|
||||||
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
|
proc parseIntSafe*(s: string, value: var int) {.noSideEffect.} =
|
||||||
noSideEffect.} =
|
|
||||||
## parses `s` into an integer in the range `validRange`. If successful,
|
## parses `s` into an integer in the range `validRange`. If successful,
|
||||||
## `value` is modified to contain the result. Otherwise no exception is
|
## `value` is modified to contain the result. Otherwise no exception is
|
||||||
## raised and `value` is not touched; this way a reasonable default value
|
## raised and `value` is not touched; this way a reasonable default value
|
||||||
## won't be overwritten.
|
## won't be overwritten.
|
||||||
var x = value
|
|
||||||
try:
|
try:
|
||||||
discard parseutils.parseInt(s, x, 0)
|
discard parseutils.parseInt(s, value, 0)
|
||||||
except OverflowError:
|
except OverflowError:
|
||||||
discard
|
discard
|
||||||
if x in validRange: value = x
|
|
||||||
|
|
||||||
proc getInt*(s: string, default = 0): int =
|
proc getInt*(s: string, default = 0): int =
|
||||||
## Safely parses an int and returns it.
|
## Safely parses an int and returns it.
|
||||||
result = default
|
result = default
|
||||||
parseInt(s, result, 0..1_000_000_000)
|
parseIntSafe(s, result)
|
||||||
|
|
||||||
|
proc getInt64*(s: string, default = 0): int64 =
|
||||||
|
## Safely parses an int and returns it.
|
||||||
|
result = default
|
||||||
|
try:
|
||||||
|
discard parseutils.parseBiggestInt(s, result, 0)
|
||||||
|
except OverflowError:
|
||||||
|
discard
|
||||||
|
|
||||||
when defined(js):
|
when defined(js):
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
|
|
@ -32,7 +37,8 @@ when defined(js):
|
||||||
for class in classes:
|
for class in classes:
|
||||||
if class.present: result.add(class.name & " ")
|
if class.present: result.add(class.name & " ")
|
||||||
|
|
||||||
proc makeUri*(relative: string, appName=appName, includeHash=false): string =
|
proc makeUri*(relative: string, appName=appName, includeHash=false,
|
||||||
|
search: string=""): string =
|
||||||
## Concatenates ``relative`` to the current URL in a way that is
|
## Concatenates ``relative`` to the current URL in a way that is
|
||||||
## (possibly) sane.
|
## (possibly) sane.
|
||||||
var relative = relative
|
var relative = relative
|
||||||
|
|
@ -43,7 +49,7 @@ when defined(js):
|
||||||
$window.location.host &
|
$window.location.host &
|
||||||
appName &
|
appName &
|
||||||
relative &
|
relative &
|
||||||
$window.location.search &
|
search &
|
||||||
(if includeHash: $window.location.hash else: "")
|
(if includeHash: $window.location.hash else: "")
|
||||||
|
|
||||||
proc makeUri*(relative: string, params: varargs[(string, string)],
|
proc makeUri*(relative: string, params: varargs[(string, string)],
|
||||||
|
|
@ -55,7 +61,11 @@ when defined(js):
|
||||||
query.add(param[0] & "=" & param[1])
|
query.add(param[0] & "=" & param[1])
|
||||||
|
|
||||||
if query.len > 0:
|
if query.len > 0:
|
||||||
makeUri(relative & "?" & query, appName)
|
var search = $window.location.search
|
||||||
|
if search.len != 0: search.add("&")
|
||||||
|
search.add(query)
|
||||||
|
if search[0] != '?': search = "?" & search
|
||||||
|
makeUri(relative, appName, search=search)
|
||||||
else:
|
else:
|
||||||
makeUri(relative, appName)
|
makeUri(relative, appName)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ when defined(js):
|
||||||
tdiv(class="content"):
|
tdiv(class="content"):
|
||||||
input(class="form-input", `type`="text", name="subject",
|
input(class="form-input", `type`="text", name="subject",
|
||||||
placeholder="Type the title here",
|
placeholder="Type the title here",
|
||||||
onChange=(e: Event, n: VNode) => onSubjectChange(e, n, state))
|
oninput=(e: Event, n: VNode) => onSubjectChange(e, n, state))
|
||||||
if state.error.isSome():
|
if state.error.isSome():
|
||||||
p(class="text-error"):
|
p(class="text-error"):
|
||||||
text state.error.get().message
|
text state.error.get().message
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ when defined(js):
|
||||||
var formData = newFormData()
|
var formData = newFormData()
|
||||||
formData.append("email", email)
|
formData.append("email", email)
|
||||||
result = newPostButton(
|
result = newPostButton(
|
||||||
makeUri("/resetPassword"),
|
makeUri("/sendResetPassword"),
|
||||||
formData,
|
formData,
|
||||||
"Send password reset email",
|
"Send password reset email",
|
||||||
"fas fa-envelope",
|
"fas fa-envelope",
|
||||||
|
|
|
||||||
65
src/frontend/resetpassword.nim
Normal file
65
src/frontend/resetpassword.nim
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
when defined(js):
|
||||||
|
import sugar, httpcore, options, json
|
||||||
|
import dom except Event
|
||||||
|
|
||||||
|
include karax/prelude
|
||||||
|
import karax / [kajax, kdom]
|
||||||
|
|
||||||
|
import error, replybox, threadlist, post
|
||||||
|
import karaxutils
|
||||||
|
|
||||||
|
type
|
||||||
|
ResetPassword* = ref object
|
||||||
|
loading: bool
|
||||||
|
status: HttpCode
|
||||||
|
error: Option[PostError]
|
||||||
|
newPassword: kstring
|
||||||
|
|
||||||
|
proc newResetPassword*(): ResetPassword =
|
||||||
|
ResetPassword(
|
||||||
|
status: Http200,
|
||||||
|
newPassword: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
proc onPassChange(e: Event, n: VNode, state: ResetPassword) =
|
||||||
|
state.newPassword = n.value
|
||||||
|
|
||||||
|
proc onPost(httpStatus: int, response: kstring, state: ResetPassword) =
|
||||||
|
postFinished:
|
||||||
|
navigateTo(makeUri("/resetPassword/success"))
|
||||||
|
|
||||||
|
proc onSetClick(
|
||||||
|
ev: Event, n: VNode,
|
||||||
|
state: ResetPassword
|
||||||
|
) =
|
||||||
|
state.loading = true
|
||||||
|
state.error = none[PostError]()
|
||||||
|
|
||||||
|
let uri = makeUri("resetPassword", ("newPassword", $state.newPassword))
|
||||||
|
ajaxPost(uri, @[], "",
|
||||||
|
(s: int, r: kstring) => onPost(s, r, state))
|
||||||
|
|
||||||
|
proc render*(state: ResetPassword): VNode =
|
||||||
|
if state.loading:
|
||||||
|
return buildHtml(tdiv(class="loading"))
|
||||||
|
|
||||||
|
result = buildHtml():
|
||||||
|
section(class="container grid-xl"):
|
||||||
|
tdiv(class="resetpassword"):
|
||||||
|
tdiv(class="title"):
|
||||||
|
p(): text "Reset Password"
|
||||||
|
tdiv(class="content"):
|
||||||
|
input(class="form-input", `type`="password", name="password",
|
||||||
|
placeholder="Type your new password here",
|
||||||
|
oninput=(e: Event, n: VNode) => onPassChange(e, n, state))
|
||||||
|
if state.error.isSome():
|
||||||
|
p(class="text-error"):
|
||||||
|
text state.error.get().message
|
||||||
|
tdiv(class="footer"):
|
||||||
|
button(class=class(
|
||||||
|
{"loading": state.loading},
|
||||||
|
"btn btn-primary"
|
||||||
|
),
|
||||||
|
onClick=(ev: Event, n: VNode) =>
|
||||||
|
(onSetClick(ev, n, state))):
|
||||||
|
text "Set password"
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs,
|
import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs,
|
||||||
htmlparser, streams, parseutils, options
|
htmlparser, streams, parseutils, options, logging
|
||||||
from times import getTime, getGMTime, format
|
from times import getTime, getGMTime, format
|
||||||
|
|
||||||
# Used to be:
|
# Used to be:
|
||||||
|
|
@ -178,4 +178,4 @@ proc rstToHtml*(content: string): string =
|
||||||
result = ""
|
result = ""
|
||||||
add(result, node, indWidth=0, addNewLines=false)
|
add(result, node, indWidth=0, addNewLines=false)
|
||||||
except:
|
except:
|
||||||
echo("[WARNING] Could not parse rst html.")
|
warn("Could not parse rst html.")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue