Email activation and password resets for users.
This commit is contained in:
parent
5d0784c55b
commit
05a529df5b
4 changed files with 224 additions and 20 deletions
42
forms.tmpl
42
forms.tmpl
|
|
@ -10,7 +10,7 @@
|
|||
# const threadId = 0
|
||||
# const name = 1
|
||||
# const views = 2
|
||||
#
|
||||
#
|
||||
# result = ""
|
||||
# count = 0
|
||||
<div id="talk-heads">
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
</div>
|
||||
|
||||
#let users = getAllRows(db,
|
||||
# sql("select distinct name, email from person where id in " &
|
||||
# sql("select distinct name, email from person where id in " &
|
||||
# "(select author from post where thread = ?)"), %threadId)
|
||||
<div class="users">
|
||||
<div>
|
||||
|
|
@ -55,9 +55,9 @@
|
|||
#end for
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
#let latestReplyAuthor = getValue(db, sql("select name from person where id = " &
|
||||
# "(select author from post where id = " &
|
||||
# "(select author from post where id = " &
|
||||
# "(select max(id) from post where thread = ?))"), %threadId)
|
||||
|
||||
#let replyProfileUrl = c.req.makeUri("profile/", false) &
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
#
|
||||
#
|
||||
#proc genPostsList(c: var TForumData, threadId: string, count: var int): string =
|
||||
# const query = sql"""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p,
|
||||
# const query = sql"""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p,
|
||||
# person u where u.id = p.author and p.thread = ? order by p.id limit ?, ?"""
|
||||
# const postId = 0
|
||||
# const userName = 1
|
||||
|
|
@ -204,7 +204,7 @@
|
|||
<br/>
|
||||
<input type="submit" name="previewBtn" value="Preview" />
|
||||
<input type="submit" name="postBtn" value="Submit" />
|
||||
|
||||
|
||||
<a href="http://nim-lang.org/rst.html">Syntax Cheatsheet</a>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -367,3 +367,33 @@
|
|||
# end if
|
||||
#end proc
|
||||
#
|
||||
#
|
||||
#proc genFormResetPassword(c: var TForumData): string =
|
||||
# result = ""
|
||||
<div id="talk-head">
|
||||
<div class="info-post">
|
||||
<div>
|
||||
<a href="${c.req.makeUri("/")}"><b>forum index</b></a> >
|
||||
Reset Password
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form action="${c.req.makeUri("/doresetpassword", false)}" method="POST">
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td>${FieldValid(c, "nick", "Your nickname:")}</td>
|
||||
<td><input type="text" name="nick" maxlength="20" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td>
|
||||
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
#if c.errorMsg != "":
|
||||
<div style="float: left; width: 100%;">
|
||||
<span class="error">$c.errorMsg</span>
|
||||
</div>
|
||||
#end if
|
||||
<input type="submit" value="Email me">
|
||||
</form>
|
||||
#end proc
|
||||
|
|
|
|||
187
forum.nim
187
forum.nim
|
|
@ -8,12 +8,13 @@
|
|||
|
||||
import
|
||||
os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers,
|
||||
rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils
|
||||
rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
|
||||
parseutils, utils
|
||||
|
||||
when not defined(windows):
|
||||
import bcrypt # TODO
|
||||
|
||||
from htmlgen import tr, th, td, span
|
||||
from htmlgen import tr, th, td, span, input
|
||||
|
||||
const
|
||||
unselectedThread = -1
|
||||
|
|
@ -25,6 +26,7 @@ const
|
|||
noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"]
|
||||
noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"]
|
||||
banReasonDeactivated = "DEACTIVATED"
|
||||
banReasonEmailUnconfirmed = "EMAILCONFIRMATION"
|
||||
|
||||
type
|
||||
TCrud = enum crCreate, crRead, crUpdate, crDelete
|
||||
|
|
@ -50,6 +52,7 @@ type
|
|||
totalPosts: int
|
||||
search: string
|
||||
noPagenumumNav: bool
|
||||
config: Config
|
||||
|
||||
TStyledButton = tuple[text: string, link: string]
|
||||
|
||||
|
|
@ -72,6 +75,7 @@ var
|
|||
db: TDbConn
|
||||
docConfig: StringTableRef
|
||||
isFTSAvailable: bool
|
||||
config: Config
|
||||
|
||||
proc init(c: var TForumData) =
|
||||
c.userPass = ""
|
||||
|
|
@ -246,6 +250,20 @@ proc makePassword(password, salt: string, comparingTo = ""): string =
|
|||
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
|
||||
result = hash(getMD5(salt & getMD5(password)), bcryptSalt)
|
||||
|
||||
proc makeIdentHash(user, password, epoch, secret: string,
|
||||
comparingTo = ""): string =
|
||||
## Creates a hash verifying the identity of a user. Used for password reset
|
||||
## links and email activation links.
|
||||
## If ``epoch`` is smaller than the epoch of the user's last login then
|
||||
## the link is invalid.
|
||||
## The ``secret`` is the 'salt' field in the ``person`` table.
|
||||
echo(user, password, epoch, secret)
|
||||
when defined(windows):
|
||||
result = getMD5(user & password & epoch & secret)
|
||||
else:
|
||||
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
|
||||
result = hash(user & password & epoch & secret, bcryptSalt)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
template `||`(x: expr): expr = (if not isNil(x): x else: "")
|
||||
|
||||
|
|
@ -274,7 +292,14 @@ proc setError(c: var TForumData, field, msg: string): bool {.inline.} =
|
|||
c.errorMsg = "Error: " & msg
|
||||
return false
|
||||
|
||||
proc register(c: var TForumData, name, pass, antibot, email: string): bool =
|
||||
proc isCaptchaCorrect(c: var TForumData, antibot: string): bool =
|
||||
## Determines whether the user typed in the captcha correctly.
|
||||
let correctRes = getValue(db,
|
||||
sql"select answer from antibot where ip = ?", c.req.ip)
|
||||
return antibot == correctRes
|
||||
|
||||
proc register(c: var TForumData, name, pass, antibot,
|
||||
email: string): bool =
|
||||
# Username validation:
|
||||
if name.len == 0 or not allCharsInSet(name, SecureChars):
|
||||
return setError(c, "name", "Invalid username!")
|
||||
|
|
@ -285,25 +310,68 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool =
|
|||
if pass.len < 4:
|
||||
return setError(c, "new_password", "Invalid password!")
|
||||
|
||||
# antibot validation:
|
||||
let correctRes = getValue(db,
|
||||
sql"select answer from antibot where ip = ?", c.req.ip)
|
||||
if antibot != correctRes:
|
||||
return setError(c, "antibot", "You seem to be a bot!")
|
||||
# captcha validation:
|
||||
if not isCaptchaCorrect(c, antibot):
|
||||
return setError(c, "antibot", "Answer to captcha incorrect!")
|
||||
|
||||
# email validation
|
||||
if not validEmailAddress(email):
|
||||
if not ('@' in email and '.' in email):
|
||||
return setError(c, "email", "Invalid email address")
|
||||
|
||||
# perform registration:
|
||||
var salt = makeSalt()
|
||||
let password = makePassword(pass, salt)
|
||||
|
||||
# Send activation email.
|
||||
let epoch = $int(epochTime())
|
||||
let activateUrl = c.req.makeUri("/activateEmail?nick=$1&epoch=$2&ident=$3" %
|
||||
[encodeUrl(name), encodeUrl(epoch),
|
||||
encodeUrl(makeIdentHash(name, password, epoch, salt))])
|
||||
|
||||
let emailSentFut = sendEmailActivation(c.config, email, name, activateUrl)
|
||||
# Block until we send the email.
|
||||
# TODO: This is a workaround for 'var T' not being usable in async procs.
|
||||
while not emailSentFut.finished:
|
||||
poll()
|
||||
if emailSentFut.failed:
|
||||
echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg)
|
||||
return setError(c, "email", "Couldn't send activation email")
|
||||
|
||||
# add account to person table
|
||||
exec(db,
|
||||
sql("INSERT INTO person(name, password, email, salt, status, lastOnline, " &
|
||||
"ban) VALUES (?, ?, ?, ?, 'user', DATETIME('now'), '')"), name,
|
||||
makePassword(pass, salt), email, salt)
|
||||
# return setError(c, "", "Could not create your account!")
|
||||
"ban) VALUES (?, ?, ?, ?, 'user', DATETIME('now'), ?)"), name,
|
||||
password, email, salt,
|
||||
banReasonEmailUnconfirmed)
|
||||
|
||||
return true
|
||||
|
||||
proc resetPassword(c: var TForumData, nick, antibot: string): bool =
|
||||
# Validate captcha
|
||||
if not isCaptchaCorrect(c, antibot):
|
||||
return setError(c, "antibot", "Answer to captcha incorrect!")
|
||||
# Gather some extra information to determine ident hash.
|
||||
let epoch = $int(epochTime())
|
||||
let row = db.getRow(
|
||||
sql"select password, salt, email from person where name = ?", nick)
|
||||
if row[0] == "":
|
||||
return setError(c, "nick", "Nickname not found")
|
||||
# Generate URL for the email.
|
||||
# TODO: Get rid of the stupid `%` in main.tmpl as it screws up strutils.%
|
||||
let resetUrl = c.req.makeUri(
|
||||
strutils.`%`("/emailResetPassword?nick=$1&epoch=$2&ident=$3",
|
||||
[encodeUrl(nick), encodeUrl(epoch),
|
||||
encodeUrl(makeIdentHash(nick, row[0], epoch, row[1]))]))
|
||||
echo "User's reset URL is: ", resetUrl
|
||||
# Send the email.
|
||||
let emailSentFut = sendPassReset(c.config, row[2], nick, resetUrl)
|
||||
# TODO: This is a workaround for 'var T' not being usable in async procs.
|
||||
while not emailSentFut.finished:
|
||||
poll()
|
||||
if emailSentFut.failed:
|
||||
echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg)
|
||||
return setError(c, "email", "Couldn't send activation email")
|
||||
|
||||
proc checkLoggedIn(c: var TForumData) =
|
||||
let pass = c.req.cookies["sid"]
|
||||
if pass.len == 0: return
|
||||
|
|
@ -519,6 +587,15 @@ proc login(c: var TForumData, name, pass: string): bool =
|
|||
else:
|
||||
return c.setError("password", "Login failed!")
|
||||
|
||||
proc verifyIdentHash(c: var TForumData, name, epoch, ident: string): bool =
|
||||
const query =
|
||||
sql"select password, salt, strftime('%s', lastOnline) from person where name = ?"
|
||||
var row = getRow(db, query, name)
|
||||
if row[0] == "": return false
|
||||
let newIdent = makeIdentHash(name, row[0], epoch, row[1], ident)
|
||||
if row[2].parseInt > epoch.parseInt: return false
|
||||
result = newIdent == ident
|
||||
|
||||
proc setBan(c: var TForumData, nick, reason: string): bool =
|
||||
const query =
|
||||
sql("update person set ban = ? where name = ?")
|
||||
|
|
@ -762,6 +839,8 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string =
|
|||
td(case ui.ban
|
||||
of banReasonDeactivated:
|
||||
"Deactivated"
|
||||
of banReasonEmailUnconfirmed:
|
||||
"Awaiting email confirmation"
|
||||
of "":
|
||||
"Active"
|
||||
else:
|
||||
|
|
@ -789,6 +868,9 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string =
|
|||
elif ui.ban == banReasonDeactivated:
|
||||
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"),
|
||||
"Activate user")
|
||||
elif ui.ban == banReasonEmailUnconfirmed:
|
||||
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"),
|
||||
"Confirm user's email")
|
||||
else: ""
|
||||
else: "")
|
||||
)
|
||||
|
|
@ -814,6 +896,7 @@ template createTFD(): stmt =
|
|||
c.startTime = epochTime()
|
||||
c.isThreadsList = false
|
||||
c.pageNum = 1
|
||||
c.config = config
|
||||
if request.cookies.len > 0:
|
||||
checkLoggedIn(c)
|
||||
|
||||
|
|
@ -950,8 +1033,9 @@ routes:
|
|||
post "/doregister":
|
||||
createTFD()
|
||||
if c.register(@"name", @"new_password", @"antibot", @"email"):
|
||||
discard c.login(@"name", @"new_password")
|
||||
finishLogin()
|
||||
resp genMain(c, "You are now registered. You must now confirm your" &
|
||||
" email address by clicking the link sent to " & @"email",
|
||||
"Registration successful - Nim Forum")
|
||||
else:
|
||||
resp c.genMain(genFormRegister(c))
|
||||
|
||||
|
|
@ -1060,6 +1144,80 @@ routes:
|
|||
else:
|
||||
resp genMain(c, "Failure", "Nim Forum")
|
||||
|
||||
get "/activateEmail/?":
|
||||
createTFD()
|
||||
cond (@"nick" != "")
|
||||
cond (@"epoch" != "")
|
||||
cond (@"ident" != "")
|
||||
var epoch: BiggestInt = 0
|
||||
cond(parseBiggestInt(@"epoch", epoch) > 0)
|
||||
var success = false
|
||||
if verifyIdentHash(c, @"nick", $epoch, @"ident"):
|
||||
let ban = db.getValue(sql"select ban from person where name = ?", @"nick")
|
||||
if ban == banReasonEmailUnconfirmed:
|
||||
success = setBan(c, @"nick", "")
|
||||
|
||||
if success:
|
||||
resp genMain(c, "Account activated", "Nim Forum")
|
||||
else:
|
||||
resp genMain(c, "Account activation failed", "Nim Forum")
|
||||
|
||||
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:
|
||||
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"):
|
||||
let res = setPassword(c, @"nick", @"password")
|
||||
if res:
|
||||
resp genMain(c, "Password reset successfully!", "Nim Forum")
|
||||
else:
|
||||
resp genMain(c, "Password reset failure", "Nim Forum")
|
||||
else:
|
||||
resp genMain(c, "Invalid ident hash", "Nim Forum")
|
||||
|
||||
get "/resetPassword/?":
|
||||
createTFD()
|
||||
|
||||
resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum")
|
||||
|
||||
post "/doresetpassword":
|
||||
createTFD()
|
||||
echo(request.params)
|
||||
cond (@"nick" != "")
|
||||
|
||||
if resetPassword(c, @"nick", @"antibot"):
|
||||
resp genMain(c, "Email sent!", "Reset Password - Nim Forum")
|
||||
else:
|
||||
resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum")
|
||||
|
||||
const licenseRst = slurp("static/license.rst")
|
||||
get "/license":
|
||||
createTFD()
|
||||
|
|
@ -1117,6 +1275,7 @@ when isMainModule:
|
|||
database="nimforum")
|
||||
isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " &
|
||||
"type='table' AND name='post_fts'")).len == 1
|
||||
config = loadConfig()
|
||||
var http = true
|
||||
if paramCount() > 0:
|
||||
if paramStr(1) == "scgi":
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
<input type="submit" style="display: none;"
|
||||
id="hdnLogin" value="Login" />
|
||||
</form>
|
||||
<a id="passreset" href=${c.req.makeUri("/resetPassword")}>Reset password</a>
|
||||
#if c.errorMsg != "" and c.req.pathInfo.normalizeUri == "/dologin":
|
||||
<span class="error">$c.errorMsg</span>
|
||||
#end if
|
||||
|
|
|
|||
|
|
@ -516,6 +516,20 @@ div#sidebar .content .search
|
|||
width: 95%;
|
||||
}
|
||||
|
||||
div#sidebar .content a#passreset {
|
||||
color: #CEDAE9;
|
||||
font-size: 9pt;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
margin-top: -4pt;
|
||||
}
|
||||
|
||||
div#sidebar .content a#passreset:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
span.error
|
||||
{
|
||||
float: left;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue