Implements logging in.
This commit is contained in:
parent
29eb22cf9c
commit
c0bbce53e9
6 changed files with 228 additions and 99 deletions
34
forum.nim
34
forum.nim
|
|
@ -7,12 +7,14 @@
|
|||
#
|
||||
|
||||
import
|
||||
os, strutils, times, md5, strtabs, cgi, math, db_sqlite,
|
||||
os, strutils, times, md5, strtabs, math, db_sqlite,
|
||||
scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
|
||||
parseutils, utils, random, rst, ranks, recaptcha, json, re
|
||||
import cgi except setCookie
|
||||
import options
|
||||
|
||||
import redesign/threadlist except User
|
||||
import redesign/[category, postlist]
|
||||
import redesign/[category, postlist, error, header]
|
||||
|
||||
when not defined(windows):
|
||||
import bcrypt # TODO
|
||||
|
|
@ -1153,6 +1155,34 @@ routes:
|
|||
|
||||
resp $(%list), "application/json"
|
||||
|
||||
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"
|
||||
)
|
||||
resp $(%err), "application/json"
|
||||
|
||||
get "/karax/status.json":
|
||||
createTFD()
|
||||
let user =
|
||||
if c.loggedIn():
|
||||
some(threadlist.User(
|
||||
name: c.username,
|
||||
avatarUrl: c.email.getGravatarUrl(),
|
||||
isOnline: true
|
||||
))
|
||||
else:
|
||||
none[threadlist.User]()
|
||||
|
||||
let status = UserStatus(user: user)
|
||||
resp $(%status), "application/json"
|
||||
|
||||
get re"/karax/(.+)?":
|
||||
resp readFile("redesign/karax.html")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
include karax/prelude
|
||||
import karax / [vstyles, kajax, kdom]
|
||||
type
|
||||
PostError* = object
|
||||
errorFields*: seq[string] ## IDs of the fields with an error.
|
||||
message*: string
|
||||
|
||||
when defined(js):
|
||||
include karax/prelude
|
||||
import karax / [vstyles, kajax, kdom]
|
||||
|
||||
proc renderError*(message: string): VNode =
|
||||
result = buildHtml():
|
||||
tdiv(class="empty error"):
|
||||
tdiv(class="empty icon"):
|
||||
italic(class="fas fa-bug fa-5x")
|
||||
p(class="empty-title h5"):
|
||||
text message
|
||||
p(class="empty-subtitle"):
|
||||
text "Please report this issue to us so we can fix it!"
|
||||
tdiv(class="empty-action"):
|
||||
a(href="https://github.com/nim-lang/nimforum/issues", target="_blank"):
|
||||
button(class="btn btn-primary"):
|
||||
text "Report issue"
|
||||
proc renderError*(message: string): VNode =
|
||||
result = buildHtml():
|
||||
tdiv(class="empty error"):
|
||||
tdiv(class="empty icon"):
|
||||
italic(class="fas fa-bug fa-5x")
|
||||
p(class="empty-title h5"):
|
||||
text message
|
||||
p(class="empty-subtitle"):
|
||||
text "Please report this issue to us so we can fix it!"
|
||||
tdiv(class="empty-action"):
|
||||
a(href="https://github.com/nim-lang/nimforum/issues", target="_blank"):
|
||||
button(class="btn btn-primary"):
|
||||
text "Report issue"
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import strformat, times, options, json, tables, future
|
||||
import strformat, times, options, json, tables, sugar
|
||||
from dom import window, Location
|
||||
|
||||
include karax/prelude
|
||||
import jester/patterns
|
||||
|
||||
import threadlist, postlist, karaxutils
|
||||
import threadlist, postlist, header
|
||||
import karaxutils
|
||||
|
||||
type
|
||||
State = ref object
|
||||
|
|
@ -24,83 +25,6 @@ proc onPopState(event: dom.Event) =
|
|||
state.url = window.location
|
||||
redraw()
|
||||
|
||||
proc genHeader(): VNode =
|
||||
result = buildHtml(tdiv()):
|
||||
header(id="main-navbar"):
|
||||
tdiv(class="navbar container grid-xl"):
|
||||
section(class="navbar-section"):
|
||||
a(href=makeUri("/")):
|
||||
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")
|
||||
a(href="#signup-modal", id="signup-btn"):
|
||||
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"
|
||||
|
||||
# Modals
|
||||
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():
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="username"):
|
||||
text "Username"
|
||||
input(class="form-input", `type`="text", id="username")
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="password"):
|
||||
text "Password"
|
||||
input(class="form-input", `type`="password", id="password")
|
||||
button(class="btn btn-link"):
|
||||
text "Reset your password"
|
||||
tdiv(class="modal-footer"):
|
||||
button(class="btn btn-primary"):
|
||||
text "Log in"
|
||||
a(href="#signup-modal"):
|
||||
button(class="btn"):
|
||||
text "Create account"
|
||||
|
||||
tdiv(class="modal", id="signup-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 "Create a new account"
|
||||
tdiv(class="modal-body"):
|
||||
tdiv(class="content"):
|
||||
form():
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="email"):
|
||||
text "Email"
|
||||
input(class="form-input", `type`="text", id="email")
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="username"):
|
||||
text "Username"
|
||||
input(class="form-input", `type`="text", id="username")
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="password"):
|
||||
text "Password"
|
||||
input(class="form-input", `type`="password", id="password")
|
||||
tdiv(class="modal-footer"):
|
||||
button(class="btn btn-primary"):
|
||||
text "Create account"
|
||||
a(href="#login-modal"):
|
||||
button(class="btn"):
|
||||
text "Log in"
|
||||
|
||||
|
||||
const appName = "/karax"
|
||||
type Params = Table[string, string]
|
||||
type
|
||||
|
|
@ -118,7 +42,7 @@ proc route(routes: openarray[Route]): VNode =
|
|||
|
||||
proc render(): VNode =
|
||||
result = buildHtml(tdiv()):
|
||||
genHeader()
|
||||
renderHeader()
|
||||
route([
|
||||
r("/t/@id?",
|
||||
(params: Params) =>
|
||||
|
|
|
|||
157
redesign/header.nim
Normal file
157
redesign/header.nim
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import options, times, httpcore, json, sugar
|
||||
|
||||
import threadlist
|
||||
type
|
||||
UserStatus* = object
|
||||
user*: Option[User]
|
||||
|
||||
when defined(js):
|
||||
include karax/prelude
|
||||
import karax / [kajax]
|
||||
|
||||
|
||||
import karaxutils
|
||||
|
||||
from dom import setTimeout, window, document, getElementById
|
||||
|
||||
type
|
||||
State = ref object
|
||||
data: Option[UserStatus]
|
||||
loading: bool
|
||||
status: HttpCode
|
||||
lastUpdate: Time
|
||||
|
||||
proc newState(): State =
|
||||
State(
|
||||
data: none[UserStatus](),
|
||||
loading: false,
|
||||
status: Http200
|
||||
)
|
||||
|
||||
var
|
||||
state = newState()
|
||||
|
||||
proc getStatus
|
||||
proc onStatus(httpStatus: int, response: kstring) =
|
||||
state.loading = false
|
||||
state.status = httpStatus.HttpCode
|
||||
if state.status != Http200: return
|
||||
|
||||
let parsed = parseJson($response)
|
||||
state.data = some(to(parsed, UserStatus))
|
||||
|
||||
state.lastUpdate = getTime()
|
||||
|
||||
proc getStatus =
|
||||
if state.loading: return
|
||||
let diff = getTime() - state.lastUpdate
|
||||
if diff.minutes < 5:
|
||||
return
|
||||
|
||||
state.loading = true
|
||||
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"):
|
||||
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 "Create a new account"
|
||||
tdiv(class="modal-body"):
|
||||
tdiv(class="content"):
|
||||
form():
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="email"):
|
||||
text "Email"
|
||||
input(class="form-input", `type`="text", name="email")
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="regusername"):
|
||||
text "Username"
|
||||
input(class="form-input", `type`="text", name="username")
|
||||
tdiv(class="form-group"):
|
||||
label(class="form-label", `for`="regpassword"):
|
||||
text "Password"
|
||||
input(class="form-input", `type`="password", name="password")
|
||||
tdiv(class="modal-footer"):
|
||||
button(class="btn btn-primary"):
|
||||
text "Create account"
|
||||
a(href="#login-modal"):
|
||||
button(class="btn"):
|
||||
text "Log in"
|
||||
|
||||
proc renderHeader*(): VNode =
|
||||
if state.data.isNone:
|
||||
getStatus()
|
||||
|
||||
let user = state.data.map(x => x.user).flatten
|
||||
result = buildHtml(tdiv()): # TODO: Why do some buildHtml's need this?
|
||||
header(id="main-navbar"):
|
||||
tdiv(class="navbar container grid-xl"):
|
||||
section(class="navbar-section"):
|
||||
a(href=makeUri("/")):
|
||||
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")
|
||||
if state.loading:
|
||||
tdiv(class="loading")
|
||||
elif user.isNone:
|
||||
a(href="#signup-modal", id="signup-btn"):
|
||||
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"
|
||||
else:
|
||||
render(user.get(), "avatar")
|
||||
|
||||
# Modals
|
||||
genLoginModal()
|
||||
|
||||
genSignUpModal()
|
||||
|
|
@ -48,4 +48,12 @@ proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb?
|
|||
dom.pushState(dom.window.history, 5, cstring"Thread", url)
|
||||
|
||||
# Fire the popState event.
|
||||
dom.window.dispatchEvent(newEvent("popstate"))
|
||||
dom.window.dispatchEvent(newEvent("popstate"))
|
||||
|
||||
|
||||
type
|
||||
FormData* = ref object
|
||||
proc newFormData*(form: dom.Element): FormData
|
||||
{.importcpp: "new FormData(@)", constructor.}
|
||||
proc get*(form: FormData, key: cstring): cstring
|
||||
{.importcpp: "#.get(@)".}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs,
|
||||
htmlparser, streams, parseutils
|
||||
htmlparser, streams, parseutils, options
|
||||
from times import getTime, getGMTime, format
|
||||
|
||||
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
|
||||
|
|
@ -20,6 +20,11 @@ proc getInt*(s: string, default = 0): int =
|
|||
result = default
|
||||
parseInt(s, result, 0..1_000_000_000)
|
||||
|
||||
proc `%`*[T](opt: Option[T]): JsonNode =
|
||||
## Generic constructor for JSON data. Creates a new ``JNull JsonNode``
|
||||
## if ``opt`` is empty, otherwise it delegates to the underlying value.
|
||||
if opt.isSome: %opt.get else: newJNull()
|
||||
|
||||
type
|
||||
Config* = object
|
||||
smtpAddress: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue