Implements registration. Refactors login backend code.
This commit is contained in:
parent
fe7c39b538
commit
3a394386d4
7 changed files with 146 additions and 138 deletions
234
forum.nim
234
forum.nim
|
|
@ -14,7 +14,9 @@ import cgi except setCookie
|
|||
import options
|
||||
|
||||
import redesign/threadlist except User
|
||||
import redesign/[category, postlist, error, header, post, profile, user]
|
||||
import redesign/[
|
||||
category, postlist, error, header, post, profile, user, karaxutils
|
||||
]
|
||||
|
||||
when not defined(windows):
|
||||
import bcrypt # TODO
|
||||
|
|
@ -297,63 +299,6 @@ proc setError(c: TForumData, field, msg: string): bool {.inline.} =
|
|||
c.errorMsg = "Error: " & msg
|
||||
return false
|
||||
|
||||
proc register(c: TForumData, name, pass, antibot, userIp,
|
||||
email: string): Future[bool] {.async.} =
|
||||
# Username validation:
|
||||
if name.len == 0 or not allCharsInSet(name, UsernameIdent):
|
||||
return setError(c, "name", "Invalid username!")
|
||||
if getValue(db, sql"select name from person where name = ?", name).len > 0:
|
||||
return setError(c, "name", "Username already exists!")
|
||||
|
||||
# Password validation:
|
||||
if pass.len < 4:
|
||||
return setError(c, "new_password", "Invalid password!")
|
||||
|
||||
# captcha validation:
|
||||
if useCaptcha:
|
||||
var captchaValid: bool = false
|
||||
try:
|
||||
captchaValid = await captcha.verify(antibot, userIp)
|
||||
except:
|
||||
echo("[ERROR] Error checking captcha: " & getCurrentExceptionMsg())
|
||||
captchaValid = false
|
||||
|
||||
if not captchaValid:
|
||||
return setError(c, "g-recaptcha-response", "Answer to captcha incorrect!")
|
||||
|
||||
# email validation
|
||||
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()
|
||||
when not defined(dev):
|
||||
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) " &
|
||||
"VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name,
|
||||
password, email, salt,
|
||||
when defined(dev): $Moderated else: $EmailUnconfirmed)
|
||||
|
||||
return true
|
||||
|
||||
proc resetPassword(c: TForumData, nick, antibot, userIp: string): Future[bool] {.async.} =
|
||||
# captcha validation:
|
||||
if useCaptcha:
|
||||
|
|
@ -678,34 +623,6 @@ proc newThread(c: TForumData): bool =
|
|||
threadUrl=c.makeThreadURL())
|
||||
result = true
|
||||
|
||||
proc login(c: TForumData, name, pass: string): bool =
|
||||
# get form data:
|
||||
const query =
|
||||
sql"select id, name, password, email, salt, status, ban from person where name = ?"
|
||||
if name.len == 0:
|
||||
return c.setError("name", "Username cannot be nil.")
|
||||
var success = false
|
||||
for row in fastRows(db, query, name):
|
||||
if row[2] == makePassword(pass, row[4], row[2]):
|
||||
c.rank = parseEnum[Rank](row[5])
|
||||
let ban = getBanErrorMsg(row[6], c.rank)
|
||||
if ban.len > 0:
|
||||
return c.setError("name", ban)
|
||||
c.userid = row[0]
|
||||
c.username = row[1]
|
||||
c.userpass = row[2]
|
||||
c.email = row[3]
|
||||
success = true
|
||||
break
|
||||
if success:
|
||||
# create session:
|
||||
exec(db,
|
||||
sql"insert into session (ip, password, userid) values (?, ?, ?)",
|
||||
c.req.ip, c.userpass, c.userid)
|
||||
return true
|
||||
else:
|
||||
return c.setError("password", "Login failed!")
|
||||
|
||||
proc verifyIdentHash(c: TForumData, name, epoch, ident: string): bool =
|
||||
const query =
|
||||
sql"select password, salt, strftime('%s', lastOnline) from person where name = ?"
|
||||
|
|
@ -1154,6 +1071,81 @@ proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) =
|
|||
discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')")
|
||||
discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')")
|
||||
|
||||
proc executeLogin(c: TForumData, username, password: string): string =
|
||||
## Performs a login with the specified details.
|
||||
##
|
||||
## Optionally, `username` may contain the email of the user instead.
|
||||
const query =
|
||||
sql"""
|
||||
select id, name, password, email, salt
|
||||
from person where name = ? or email = ?
|
||||
"""
|
||||
if username.len == 0:
|
||||
raise newForumError("Username cannot be empty", @["username"])
|
||||
|
||||
for row in fastRows(db, query, username, username):
|
||||
if row[2] == makePassword(password, row[4], row[2]):
|
||||
exec(
|
||||
db,
|
||||
sql"insert into session (ip, password, userid) values (?, ?, ?)",
|
||||
c.req.ip, row[2], row[0]
|
||||
)
|
||||
return row[2]
|
||||
|
||||
raise newForumError("Invalid username or password")
|
||||
|
||||
proc executeRegister(c: TForumData, name, pass, antibot, userIp,
|
||||
email: string): Future[string] {.async.} =
|
||||
## Registers a new user and returns a new session key for that user's
|
||||
## session if registration was successful. Exceptions are raised otherwise.
|
||||
|
||||
# Username validation:
|
||||
if name.len == 0 or not allCharsInSet(name, UsernameIdent):
|
||||
raise newForumError("Invalid username", @["username"])
|
||||
if getValue(db, sql"select name from person where name = ?", name).len > 0:
|
||||
raise newForumError("Username already exists", @["username"])
|
||||
|
||||
# Password validation:
|
||||
if pass.len < 4:
|
||||
raise newForumError("Please choose a longer password", @["password"])
|
||||
|
||||
# captcha validation:
|
||||
if useCaptcha:
|
||||
var verifyFut = captcha.verify(antibot, userIp)
|
||||
yield verifyFut
|
||||
if verifyFut.failed:
|
||||
raise newForumError(
|
||||
"Invalid recaptcha answer", @[]
|
||||
)
|
||||
|
||||
# email validation
|
||||
if not ('@' in email and '.' in email):
|
||||
raise newForumError("Invalid email", @["email"])
|
||||
|
||||
# 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)
|
||||
yield emailSentFut
|
||||
if emailSentFut.failed:
|
||||
echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg)
|
||||
raise newForumError("Couldn't send activation email", @["email"])
|
||||
|
||||
# Add account to person table
|
||||
exec(db,
|
||||
sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " &
|
||||
"VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name,
|
||||
password, email, salt, $Moderated)
|
||||
|
||||
return password
|
||||
|
||||
initialise()
|
||||
|
||||
routes:
|
||||
|
|
@ -1351,15 +1343,39 @@ routes:
|
|||
post "/karax/login":
|
||||
createTFD()
|
||||
let formData = request.formData
|
||||
if login(c, formData["username"].body, formData["password"].body):
|
||||
setCookie("sid", c.userpass)
|
||||
resp Http200, "{}", "application/json"
|
||||
else:
|
||||
let err = PostError(
|
||||
errorFields: @["username", "password"],
|
||||
message: "Invalid username or password"
|
||||
cond "username" in formData
|
||||
cond "password" in formData
|
||||
try:
|
||||
let session = executeLogin(
|
||||
c,
|
||||
formData["username"].body,
|
||||
formData["password"].body
|
||||
)
|
||||
resp Http403, $(%err), "application/json"
|
||||
setCookie("sid", session)
|
||||
resp Http200, "{}", "application/json"
|
||||
except ForumError as exc:
|
||||
resp Http400, $(%exc.data), "application/json"
|
||||
|
||||
post "/karax/signup":
|
||||
createTFD()
|
||||
let formData = request.formData
|
||||
let username = formData["username"].body
|
||||
let password = formData["password"].body
|
||||
try:
|
||||
discard await executeRegister(
|
||||
c,
|
||||
username,
|
||||
password,
|
||||
formData["g-recaptcha-response"].body,
|
||||
request.host,
|
||||
formData["email"].body
|
||||
)
|
||||
let session = executeLogin(c, username, password)
|
||||
setCookie("sid", session)
|
||||
resp Http200, "{}", "application/json"
|
||||
except ForumError:
|
||||
let exc = (ref ForumError)(getCurrentException())
|
||||
resp Http400, $(%exc.data), "application/json"
|
||||
|
||||
get "/karax/status.json":
|
||||
createTFD()
|
||||
|
|
@ -1377,7 +1393,14 @@ routes:
|
|||
else:
|
||||
none[User]()
|
||||
|
||||
let status = UserStatus(user: user)
|
||||
let status = UserStatus(
|
||||
user: user,
|
||||
recaptchaSiteKey:
|
||||
if useCaptcha:
|
||||
some(config.recaptchaSiteKey)
|
||||
else:
|
||||
none[string]()
|
||||
)
|
||||
resp $(%status), "application/json"
|
||||
|
||||
post "/karax/preview":
|
||||
|
|
@ -1566,27 +1589,6 @@ routes:
|
|||
resp genMain(c, body(), "Nim Forum - " &
|
||||
(if c.isPreview: "Preview" else: "Error"))
|
||||
|
||||
post "/dologin":
|
||||
createTFD()
|
||||
if login(c, @"name", @"password"):
|
||||
finishLogin()
|
||||
else:
|
||||
c.isThreadsList = true
|
||||
var count = 0
|
||||
let threadList = genThreadsList(c, count)
|
||||
let data = genMain(c, threadList,
|
||||
additionalHeaders = genRSSHeaders(c), showRssLinks = true)
|
||||
resp data
|
||||
|
||||
post "/doregister":
|
||||
createTFD()
|
||||
if await c.register(@"name", @"new_password", @"g-recaptcha-response", request.host, @"email"):
|
||||
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))
|
||||
|
||||
post "/donewthread":
|
||||
createTFD()
|
||||
if newThread(c):
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ when defined(js):
|
|||
|
||||
if not error.isNone:
|
||||
let e = error.get()
|
||||
if (e.errorFields.len == 1 and e.errorFields[0] == name) or isLast:
|
||||
if (e.errorFields.len == 1 and e.errorFields[0] == name) or
|
||||
(isLast and e.errorFields.len == 0):
|
||||
p(class="form-input-hint"):
|
||||
text e.message
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import threadlist, user
|
|||
type
|
||||
UserStatus* = object
|
||||
user*: Option[User]
|
||||
recaptchaSiteKey*: Option[string]
|
||||
|
||||
when defined(js):
|
||||
include karax/prelude
|
||||
|
|
@ -104,4 +105,5 @@ when defined(js):
|
|||
# Modals
|
||||
render(state.loginModal)
|
||||
|
||||
render(state.signupModal)
|
||||
if state.data.isSome():
|
||||
render(state.signupModal, state.data.get().recaptchaSiteKey)
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="/karax/nimcache/forum.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import strutils, options, strformat, parseutils
|
||||
import dom except window
|
||||
|
||||
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
|
||||
noSideEffect.} =
|
||||
|
|
@ -23,6 +22,8 @@ when defined(js):
|
|||
include karax/prelude
|
||||
import karax / [kdom]
|
||||
|
||||
import dom except window
|
||||
|
||||
const appName = "/karax/"
|
||||
|
||||
proc class*(classes: varargs[tuple[name: string, present: bool]],
|
||||
|
|
|
|||
|
|
@ -553,4 +553,12 @@ hr {
|
|||
&:hover, &:focus {
|
||||
text-shadow: $body-font-color 0px 0px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
// - Sign up modal
|
||||
|
||||
#signup-modal {
|
||||
.modal-container .modal-body {
|
||||
max-height: 60vh;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,29 +11,17 @@ when defined(js):
|
|||
type
|
||||
SignupModal* = ref object
|
||||
shown: bool
|
||||
loading: bool
|
||||
onSignUp, onLogIn: proc ()
|
||||
error: Option[PostError]
|
||||
|
||||
proc onSignUpPost(httpStatus: int, response: kstring, state: SignupModal) =
|
||||
let status = httpStatus.HttpCode
|
||||
if status == Http200:
|
||||
postFinished:
|
||||
state.shown = false
|
||||
state.onSignUp()
|
||||
else:
|
||||
# TODO: Karax should pass the content-type...
|
||||
try:
|
||||
let parsed = parseJson($response)
|
||||
let error = to(parsed, PostError)
|
||||
|
||||
state.error = some(error)
|
||||
except:
|
||||
kout(getCurrentExceptionMsg().cstring)
|
||||
state.error = some(PostError(
|
||||
errorFields: @[],
|
||||
message: "Unknown error occurred."
|
||||
))
|
||||
|
||||
proc onSignUpClick(ev: Event, n: VNode, state: SignupModal) =
|
||||
state.loading = true
|
||||
state.error = none[PostError]()
|
||||
|
||||
let uri = makeUri("signup")
|
||||
|
|
@ -57,9 +45,11 @@ when defined(js):
|
|||
proc show*(state: SignupModal) =
|
||||
state.shown = true
|
||||
|
||||
proc render*(state: SignupModal): VNode =
|
||||
proc render*(state: SignupModal, recaptchaSiteKey: Option[string]): VNode =
|
||||
setForeignNodeId("recaptcha")
|
||||
|
||||
result = buildHtml():
|
||||
tdiv(class=class({"active": state.shown}, "modal modal-sm"),
|
||||
tdiv(class=class({"active": state.shown}, "modal"),
|
||||
id="signup-modal"):
|
||||
a(href="", class="modal-overlay", "aria-label"="close",
|
||||
onClick=(ev: Event, n: VNode) => onClose(ev, n, state))
|
||||
|
|
@ -82,8 +72,13 @@ when defined(js):
|
|||
"password",
|
||||
true
|
||||
)
|
||||
if recaptchaSiteKey.isSome:
|
||||
tdiv(id="recaptcha"):
|
||||
tdiv(class="g-recaptcha",
|
||||
"data-sitekey"=recaptchaSiteKey.get())
|
||||
script(src="https://www.google.com/recaptcha/api.js")
|
||||
tdiv(class="modal-footer"):
|
||||
button(class="btn btn-primary",
|
||||
button(class=class({"loading": state.loading}, "btn btn-primary"),
|
||||
onClick=(ev: Event, n: VNode) => onSignUpClick(ev, n, state)):
|
||||
text "Create account"
|
||||
button(class="btn",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue