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 options
|
||||||
|
|
||||||
import redesign/threadlist except User
|
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):
|
when not defined(windows):
|
||||||
import bcrypt # TODO
|
import bcrypt # TODO
|
||||||
|
|
@ -297,63 +299,6 @@ proc setError(c: TForumData, field, msg: string): bool {.inline.} =
|
||||||
c.errorMsg = "Error: " & msg
|
c.errorMsg = "Error: " & msg
|
||||||
return false
|
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.} =
|
proc resetPassword(c: TForumData, nick, antibot, userIp: string): Future[bool] {.async.} =
|
||||||
# captcha validation:
|
# captcha validation:
|
||||||
if useCaptcha:
|
if useCaptcha:
|
||||||
|
|
@ -678,34 +623,6 @@ proc newThread(c: TForumData): bool =
|
||||||
threadUrl=c.makeThreadURL())
|
threadUrl=c.makeThreadURL())
|
||||||
result = true
|
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 =
|
proc verifyIdentHash(c: TForumData, name, epoch, ident: string): bool =
|
||||||
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 = ?"
|
||||||
|
|
@ -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(post_fts) values('optimize')")
|
||||||
discard tryExec(db, sql"insert into post_fts(thread_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()
|
initialise()
|
||||||
|
|
||||||
routes:
|
routes:
|
||||||
|
|
@ -1351,15 +1343,39 @@ routes:
|
||||||
post "/karax/login":
|
post "/karax/login":
|
||||||
createTFD()
|
createTFD()
|
||||||
let formData = request.formData
|
let formData = request.formData
|
||||||
if login(c, formData["username"].body, formData["password"].body):
|
cond "username" in formData
|
||||||
setCookie("sid", c.userpass)
|
cond "password" in formData
|
||||||
resp Http200, "{}", "application/json"
|
try:
|
||||||
else:
|
let session = executeLogin(
|
||||||
let err = PostError(
|
c,
|
||||||
errorFields: @["username", "password"],
|
formData["username"].body,
|
||||||
message: "Invalid username or password"
|
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":
|
get "/karax/status.json":
|
||||||
createTFD()
|
createTFD()
|
||||||
|
|
@ -1377,7 +1393,14 @@ routes:
|
||||||
else:
|
else:
|
||||||
none[User]()
|
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"
|
resp $(%status), "application/json"
|
||||||
|
|
||||||
post "/karax/preview":
|
post "/karax/preview":
|
||||||
|
|
@ -1566,27 +1589,6 @@ routes:
|
||||||
resp genMain(c, body(), "Nim Forum - " &
|
resp genMain(c, body(), "Nim Forum - " &
|
||||||
(if c.isPreview: "Preview" else: "Error"))
|
(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":
|
post "/donewthread":
|
||||||
createTFD()
|
createTFD()
|
||||||
if newThread(c):
|
if newThread(c):
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ when defined(js):
|
||||||
|
|
||||||
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 isLast:
|
if (e.errorFields.len == 1 and e.errorFields[0] == name) or
|
||||||
|
(isLast and e.errorFields.len == 0):
|
||||||
p(class="form-input-hint"):
|
p(class="form-input-hint"):
|
||||||
text e.message
|
text e.message
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import threadlist, user
|
||||||
type
|
type
|
||||||
UserStatus* = object
|
UserStatus* = object
|
||||||
user*: Option[User]
|
user*: Option[User]
|
||||||
|
recaptchaSiteKey*: Option[string]
|
||||||
|
|
||||||
when defined(js):
|
when defined(js):
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
|
|
@ -104,4 +105,5 @@ when defined(js):
|
||||||
# Modals
|
# Modals
|
||||||
render(state.loginModal)
|
render(state.loginModal)
|
||||||
|
|
||||||
render(state.signupModal)
|
if state.data.isSome():
|
||||||
|
render(state.signupModal, state.data.get().recaptchaSiteKey)
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="/karax/nimcache/forum.js"></script>
|
<script type="text/javascript" src="/karax/nimcache/forum.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import strutils, options, strformat, parseutils
|
import strutils, options, strformat, parseutils
|
||||||
import dom except window
|
|
||||||
|
|
||||||
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
|
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
|
||||||
noSideEffect.} =
|
noSideEffect.} =
|
||||||
|
|
@ -23,6 +22,8 @@ when defined(js):
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import karax / [kdom]
|
import karax / [kdom]
|
||||||
|
|
||||||
|
import dom except window
|
||||||
|
|
||||||
const appName = "/karax/"
|
const appName = "/karax/"
|
||||||
|
|
||||||
proc class*(classes: varargs[tuple[name: string, present: bool]],
|
proc class*(classes: varargs[tuple[name: string, present: bool]],
|
||||||
|
|
|
||||||
|
|
@ -553,4 +553,12 @@ hr {
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
text-shadow: $body-font-color 0px 0px 0px;
|
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
|
type
|
||||||
SignupModal* = ref object
|
SignupModal* = ref object
|
||||||
shown: bool
|
shown: bool
|
||||||
|
loading: bool
|
||||||
onSignUp, onLogIn: proc ()
|
onSignUp, onLogIn: proc ()
|
||||||
error: Option[PostError]
|
error: Option[PostError]
|
||||||
|
|
||||||
proc onSignUpPost(httpStatus: int, response: kstring, state: SignupModal) =
|
proc onSignUpPost(httpStatus: int, response: kstring, state: SignupModal) =
|
||||||
let status = httpStatus.HttpCode
|
postFinished:
|
||||||
if status == Http200:
|
|
||||||
state.shown = false
|
state.shown = false
|
||||||
state.onSignUp()
|
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) =
|
proc onSignUpClick(ev: Event, n: VNode, state: SignupModal) =
|
||||||
|
state.loading = true
|
||||||
state.error = none[PostError]()
|
state.error = none[PostError]()
|
||||||
|
|
||||||
let uri = makeUri("signup")
|
let uri = makeUri("signup")
|
||||||
|
|
@ -57,9 +45,11 @@ when defined(js):
|
||||||
proc show*(state: SignupModal) =
|
proc show*(state: SignupModal) =
|
||||||
state.shown = true
|
state.shown = true
|
||||||
|
|
||||||
proc render*(state: SignupModal): VNode =
|
proc render*(state: SignupModal, recaptchaSiteKey: Option[string]): VNode =
|
||||||
|
setForeignNodeId("recaptcha")
|
||||||
|
|
||||||
result = buildHtml():
|
result = buildHtml():
|
||||||
tdiv(class=class({"active": state.shown}, "modal modal-sm"),
|
tdiv(class=class({"active": state.shown}, "modal"),
|
||||||
id="signup-modal"):
|
id="signup-modal"):
|
||||||
a(href="", class="modal-overlay", "aria-label"="close",
|
a(href="", class="modal-overlay", "aria-label"="close",
|
||||||
onClick=(ev: Event, n: VNode) => onClose(ev, n, state))
|
onClick=(ev: Event, n: VNode) => onClose(ev, n, state))
|
||||||
|
|
@ -82,8 +72,13 @@ when defined(js):
|
||||||
"password",
|
"password",
|
||||||
true
|
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"):
|
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)):
|
onClick=(ev: Event, n: VNode) => onSignUpClick(ev, n, state)):
|
||||||
text "Create account"
|
text "Create account"
|
||||||
button(class="btn",
|
button(class="btn",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue