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
|
import
|
||||||
os, strutils, times, md5, strtabs, cgi, math, db_sqlite,
|
os, strutils, times, md5, strtabs, math, db_sqlite,
|
||||||
scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
|
scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
|
||||||
parseutils, utils, random, rst, ranks, recaptcha, json, re
|
parseutils, utils, random, rst, ranks, recaptcha, json, re
|
||||||
|
import cgi except setCookie
|
||||||
|
import options
|
||||||
|
|
||||||
import redesign/threadlist except User
|
import redesign/threadlist except User
|
||||||
import redesign/[category, postlist]
|
import redesign/[category, postlist, error, header]
|
||||||
|
|
||||||
when not defined(windows):
|
when not defined(windows):
|
||||||
import bcrypt # TODO
|
import bcrypt # TODO
|
||||||
|
|
@ -1153,6 +1155,34 @@ routes:
|
||||||
|
|
||||||
resp $(%list), "application/json"
|
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/(.+)?":
|
get re"/karax/(.+)?":
|
||||||
resp readFile("redesign/karax.html")
|
resp readFile("redesign/karax.html")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
include karax/prelude
|
type
|
||||||
import karax / [vstyles, kajax, kdom]
|
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 =
|
proc renderError*(message: string): VNode =
|
||||||
result = buildHtml():
|
result = buildHtml():
|
||||||
tdiv(class="empty error"):
|
tdiv(class="empty error"):
|
||||||
tdiv(class="empty icon"):
|
tdiv(class="empty icon"):
|
||||||
italic(class="fas fa-bug fa-5x")
|
italic(class="fas fa-bug fa-5x")
|
||||||
p(class="empty-title h5"):
|
p(class="empty-title h5"):
|
||||||
text message
|
text message
|
||||||
p(class="empty-subtitle"):
|
p(class="empty-subtitle"):
|
||||||
text "Please report this issue to us so we can fix it!"
|
text "Please report this issue to us so we can fix it!"
|
||||||
tdiv(class="empty-action"):
|
tdiv(class="empty-action"):
|
||||||
a(href="https://github.com/nim-lang/nimforum/issues", target="_blank"):
|
a(href="https://github.com/nim-lang/nimforum/issues", target="_blank"):
|
||||||
button(class="btn btn-primary"):
|
button(class="btn btn-primary"):
|
||||||
text "Report issue"
|
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
|
from dom import window, Location
|
||||||
|
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import jester/patterns
|
import jester/patterns
|
||||||
|
|
||||||
import threadlist, postlist, karaxutils
|
import threadlist, postlist, header
|
||||||
|
import karaxutils
|
||||||
|
|
||||||
type
|
type
|
||||||
State = ref object
|
State = ref object
|
||||||
|
|
@ -24,83 +25,6 @@ proc onPopState(event: dom.Event) =
|
||||||
state.url = window.location
|
state.url = window.location
|
||||||
redraw()
|
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"
|
const appName = "/karax"
|
||||||
type Params = Table[string, string]
|
type Params = Table[string, string]
|
||||||
type
|
type
|
||||||
|
|
@ -118,7 +42,7 @@ proc route(routes: openarray[Route]): VNode =
|
||||||
|
|
||||||
proc render(): VNode =
|
proc render(): VNode =
|
||||||
result = buildHtml(tdiv()):
|
result = buildHtml(tdiv()):
|
||||||
genHeader()
|
renderHeader()
|
||||||
route([
|
route([
|
||||||
r("/t/@id?",
|
r("/t/@id?",
|
||||||
(params: Params) =>
|
(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)
|
dom.pushState(dom.window.history, 5, cstring"Thread", url)
|
||||||
|
|
||||||
# Fire the popState event.
|
# 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,
|
import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs,
|
||||||
htmlparser, streams, parseutils
|
htmlparser, streams, parseutils, options
|
||||||
from times import getTime, getGMTime, format
|
from times import getTime, getGMTime, format
|
||||||
|
|
||||||
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
|
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
|
||||||
|
|
@ -20,6 +20,11 @@ proc getInt*(s: string, default = 0): int =
|
||||||
result = default
|
result = default
|
||||||
parseInt(s, result, 0..1_000_000_000)
|
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
|
type
|
||||||
Config* = object
|
Config* = object
|
||||||
smtpAddress: string
|
smtpAddress: string
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue