Implements proper error handling for login form.

This commit is contained in:
Dominik Picheta 2018-05-11 15:49:22 +01:00
commit f9efbe04d3
4 changed files with 143 additions and 65 deletions

View file

@ -1166,7 +1166,7 @@ routes:
errorFields: @["username", "password"],
message: "Invalid username or password"
)
resp $(%err), "application/json"
resp Http403, $(%err), "application/json"
get "/karax/status.json":
createTFD()

View file

@ -9,10 +9,10 @@ when defined(js):
include karax/prelude
import karax / [kajax]
import login
import karaxutils
from dom import setTimeout, window, document, getElementById
from dom import setTimeout, window, document, getElementById, focus
type
State = ref object
@ -20,18 +20,23 @@ when defined(js):
loading: bool
status: HttpCode
lastUpdate: Time
loginModal: LoginModal
proc newState(): State =
State(
data: none[UserStatus](),
loading: false,
status: Http200
)
proc newState(): State
var
state = newState()
proc getStatus
proc newState(): State =
State(
data: none[UserStatus](),
loading: false,
status: Http200,
loginModal: newLoginModal(
() => (state.lastUpdate = fromUnix(0); getStatus())
)
)
proc onStatus(httpStatus: int, response: kstring) =
state.loading = false
state.status = httpStatus.HttpCode
@ -52,46 +57,6 @@ when defined(js):
let uri = makeUri("status.json")
ajaxGet(uri, @[], onStatus)
proc onLogInPost(httpStatus: int, response: kstring) =
kout(response)
proc onLogInClick(ev: Event, n: VNode) =
let uri = makeUri("login")
let form = document.getElementById("login-form")
# TODO: This is a hack, karax should support this.
let formData = newFormData(form)
kout(formData.get("username"))
ajaxPost(uri, @[], cast[cstring](formData), onLogInPost)
proc genLoginModal(): VNode =
result = buildHtml():
tdiv(class="modal modal-sm", id="login-modal"):
a(href="#", class="modal-overlay", "aria-label"="close")
tdiv(class="modal-container"):
tdiv(class="modal-header"):
a(href="#", class="btn btn-clear float-right", "aria-label"="close")
tdiv(class="modal-title h5"):
text "Log in"
tdiv(class="modal-body"):
tdiv(class="content"):
form(id="login-form"):
tdiv(class="form-group"):
label(class="form-label", `for`="username"):
text "Username"
input(class="form-input", `type`="text", name="username")
tdiv(class="form-group"):
label(class="form-label", `for`="password"):
text "Password"
input(class="form-input", `type`="password", name="password")
a(href="#reset-password-modal"):
text "Reset your password"
tdiv(class="modal-footer"):
button(class="btn btn-primary", onClick=onLogInClick):
text "Log in"
a(href="#signup-modal"):
button(class="btn"):
text "Create account"
proc genSignUpModal(): VNode =
result = buildHtml():
tdiv(class="modal", id="signup-modal"):
@ -136,7 +101,9 @@ when defined(js):
img(src="images/crown.png", id="img-logo") # TODO: Customisation.
section(class="navbar-section"):
tdiv(class="input-group input-inline"):
input(class="search-input input-sm", `type`="text", placeholder="search")
input(class="search-input input-sm",
`type`="text", placeholder="search",
id="search-box")
if state.loading:
tdiv(class="loading")
elif user.isNone:
@ -144,14 +111,14 @@ when defined(js):
button(class="btn btn-primary btn-sm"):
italic(class="fas fa-user-plus")
text " Sign up"
a(href="#login-modal", id="login-btn"):
button(class="btn btn-primary btn-sm"):
italic(class="fas fa-sign-in-alt")
text " Log in"
button(class="btn btn-primary btn-sm",
onClick=(e: Event, n: VNode) => state.loginModal.show()):
italic(class="fas fa-sign-in-alt")
text " Log in"
else:
render(user.get(), "avatar")
# Modals
genLoginModal()
render(state.loginModal)
genSignUpModal()

View file

@ -12,7 +12,7 @@ proc class*(classes: varargs[tuple[name: string, present: bool]],
for class in classes:
if class.present: result.add(class.name & " ")
proc makeUri*(relative: string, appName=appName): string =
proc makeUri*(relative: string, appName=appName, includeHash=false): string =
## Concatenates ``relative`` to the current URL in a way that is sane.
var relative = relative
assert appName in $window.location.pathname
@ -23,10 +23,10 @@ proc makeUri*(relative: string, appName=appName): string =
appName &
relative &
$window.location.search &
$window.location.hash
(if includeHash: $window.location.hash else: "")
proc makeUri*(relative: string, params: varargs[(string, string)],
appName=appName): string =
appName=appName, includeHash=false): string =
var query = ""
for i in 0 ..< params.len:
let param = params[i]
@ -38,18 +38,20 @@ proc makeUri*(relative: string, params: varargs[(string, string)],
else:
makeUri(relative, appName)
proc navigateTo*(uri: cstring) =
# TODO: This was annoying. Karax also shouldn't have its own `window`.
dom.pushState(dom.window.history, 0, cstring"", uri)
# Fire the popState event.
dom.window.dispatchEvent(newEvent("popstate"))
proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb?
e.preventDefault()
# TODO: Why does Karax have it's own Node type? That's just silly.
let url = cast[dom.Node](n.dom).getAttribute(cstring"href")
# TODO: This was annoying. Karax also shouldn't have its own `window`.
dom.pushState(dom.window.history, 5, cstring"Thread", url)
# Fire the popState event.
dom.window.dispatchEvent(newEvent("popstate"))
navigateTo(url)
type
FormData* = ref object

109
redesign/login.nim Normal file
View file

@ -0,0 +1,109 @@
when defined(js):
import sugar, httpcore, options, json
import dom except Event
include karax/prelude
import karax / [kajax, kdom]
import error
import karaxutils
type
LoginModal* = ref object
shown: bool
onLogIn: proc ()
error: Option[PostError]
proc onLogInPost(httpStatus: int, response: kstring, state: LoginModal) =
let status = httpStatus.HttpCode
if status == Http200:
state.shown = false
state.onLogIn()
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 onLogInClick(ev: Event, n: VNode, state: LoginModal) =
state.error = none[PostError]()
let uri = makeUri("login")
let form = dom.document.getElementById("login-form")
# TODO: This is a hack, karax should support this.
let formData = newFormData(form)
ajaxPost(uri, @[], cast[cstring](formData),
(s: int, r: kstring) => onLogInPost(s, r, state))
proc onClose(ev: Event, n: VNode, state: LoginModal) =
state.shown = false
ev.preventDefault()
proc newLoginModal*(onLogIn: proc ()): LoginModal =
LoginModal(
shown: false,
onLogIn: onLogIn
)
proc show*(state: LoginModal) =
state.shown = true
proc genFormField(error: Option[PostError], name, label, typ: string,
isLast: bool): VNode =
let hasError =
not error.isNone and (
name in error.get().errorFields or
error.get().errorFields.len == 0)
result = buildHtml():
tdiv(class=class({"has-error": hasError}, "form-group")):
label(class="form-label", `for`=name):
text "Username"
input(class="form-input", `type`="text", name=name)
if not error.isNone:
let e = error.get()
if (e.errorFields.len == 1 and e.errorFields[0] == name) or isLast:
p(class="form-input-hint"):
text e.message
proc render*(state: LoginModal): VNode =
result = buildHtml():
tdiv(class=class({"active": state.shown}, "modal modal-sm"),
id="login-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 "Log in"
tdiv(class="modal-body"):
tdiv(class="content"):
form(id="login-form"):
genFormField(state.error, "username", "Username", "text", false)
genFormField(
state.error,
"password",
"Password",
"password",
true
)
a(href="#reset-password-modal"):
text "Reset your password"
tdiv(class="modal-footer"):
button(class="btn btn-primary",
onClick=(ev: Event, n: VNode) => onLogInClick(ev, n, state)):
text "Log in"
a(href="#signup-modal"):
button(class="btn"):
text "Create account"