212 lines
9.0 KiB
Nim
212 lines
9.0 KiB
Nim
import prologue
|
|
import prologue/middlewares/staticfile
|
|
import karax/[karaxdsl, vdom]
|
|
import tiny_sqlite
|
|
import strutils
|
|
import logging
|
|
import times
|
|
|
|
import ./util
|
|
from ./md import nil
|
|
from ./domain import nil
|
|
import ./sqlitesession
|
|
|
|
let env = loadPrologueEnv(".env")
|
|
let settings = newSettings(
|
|
appName = "xenotime",
|
|
debug = env.getOrDefault("debug", true),
|
|
port = Port(env.getOrDefault("port", 5070)),
|
|
secretKey = env.get("secretKey")
|
|
)
|
|
const dbPath = "./xenotime.sqlite3" # TODO: work out gcsafety issues in making this runtime-configurable
|
|
|
|
proc openDBConnection(): DbConn =
|
|
logger().log(lvlInfo, "Opening database connection")
|
|
let conn = openDatabase(dbPath)
|
|
conn.exec("PRAGMA foreign_keys = ON")
|
|
conn.exec("PRAGMA journal_mode = WAL")
|
|
return conn
|
|
|
|
autoInitializedThreadvar(db, DbConn, openDBConnection())
|
|
|
|
block:
|
|
let db = openDatabase(dbPath)
|
|
domain.migrate(db)
|
|
close(db())
|
|
|
|
type
|
|
AppContext* = ref object of Context
|
|
db*: DbConn
|
|
user*: Option[domain.User]
|
|
|
|
proc dbMiddleware(): HandlerAsync =
|
|
result = proc(ctx: AppContext) {.async.} =
|
|
ctx.db = db()
|
|
await switch(ctx)
|
|
|
|
proc generalMiddleware(): HandlerAsync =
|
|
result = proc(ctx: AppContext) {.async.} =
|
|
let userID = ctx.session.getOrDefault("uid")
|
|
if userID != "":
|
|
ctx.user = domain.getUser(ctx.db, parseBiggestInt userID)
|
|
await switch(ctx)
|
|
ctx.response.setHeader("X-Content-Type-Options", "nosniff")
|
|
ctx.response.setHeader("Content-Security-Policy", "default-src 'self'; img-src * data:; media-src * data:; form-action 'self'; frame-ancestors 'self'")
|
|
ctx.response.setHeader("Referrer-Policy", "origin-when-cross-origin")
|
|
|
|
func navButton(content: string, href: string, class: string): VNode = buildHtml(a(class="link-button " & class, href=href)): text content
|
|
|
|
func base(ctx: AppContext, title: string, navItems: seq[VNode], bodyItems: VNode, sidebar: Option[VNode] = none(VNode)): string =
|
|
let sidebarClass = if sidebar.isSome: "has-sidebar" else: ""
|
|
let vnode = buildHtml(html):
|
|
head:
|
|
link(rel="stylesheet", href="/static/style.css")
|
|
meta(charset="utf-8")
|
|
meta(name="viewport", content="width=device-width,initial-scale=1.0")
|
|
title: text title
|
|
body:
|
|
main(class=sidebarClass):
|
|
for (category, msg) in ctx.getFlashedMsgsWithCategory():
|
|
tdiv(class=("flashmsg " & category)):
|
|
text msg
|
|
nav:
|
|
navButton("Index", ctx.urlFor("index"), "index")
|
|
navButton("New Thread", ctx.urlFor("new-thread"), "new-thread")
|
|
if ctx.user.isNone:
|
|
navButton("Login", ctx.urlFor("login"), "login")
|
|
else:
|
|
tdiv(class="logged-in-user"): text ctx.user.get.name
|
|
for n in navItems: n
|
|
tdiv(class="header"):
|
|
h1: text title
|
|
bodyItems
|
|
if sidebar.isSome: tdiv(class="sidebar"): get sidebar
|
|
"<!DOCTYPE html>" & $vnode
|
|
|
|
proc displayTime(t: Time): string = t.format("uuuu-MM-dd HH:mm:ss", utc())
|
|
|
|
proc index(ctx: AppContext) {.async.} =
|
|
let threads = domain.allThreads(ctx.db)
|
|
let html = buildHtml(tdiv):
|
|
table(class="threads"):
|
|
tr:
|
|
th: text "Title"
|
|
th: text "Creator"
|
|
th: text "Posts"
|
|
th: text "Created"
|
|
th: text "Updated"
|
|
for thread in threads:
|
|
tr(class="thread"):
|
|
td(class="title"): a(href=ctx.urlFor("thread", {"id": $thread.id})): thread.title.text
|
|
td: thread.creatorName.text
|
|
td: thread.replies.`$`.text
|
|
td: thread.created.displayTime.text
|
|
td: thread.updated.displayTime.text
|
|
resp base(ctx, "Forum", @[ ], html)
|
|
|
|
proc viewThread(ctx: AppContext) {.async.} =
|
|
let threadID = parseBiggestInt ctx.getPathParams("id")
|
|
let thread = domain.getThread(ctx.db, threadID).get
|
|
let posts = domain.getPosts(ctx.db, threadID)
|
|
let html = buildHtml(tdiv):
|
|
tdiv(class="posts"):
|
|
for post in posts:
|
|
tdiv(class="post", id=("post" & $post.index)):
|
|
tdiv(class="heading"):
|
|
tdiv(class="username"): post.authorName.text
|
|
tdiv:
|
|
span(class="timestamp"): post.time.displayTime().text
|
|
text " "
|
|
a(href=("#post" & $post.index)): text ("#" & $post.index)
|
|
tdiv(class="md"): md.renderToHtml(post.content).verbatim
|
|
form(`method`="POST", action=ctx.urlFor("submit"), class="new-post"):
|
|
input(`type`="hidden", value= $threadID, name="thread-id")
|
|
textarea(name="content")
|
|
input(`type`="submit", value="Post", class="submit-post")
|
|
resp base(ctx, thread.title, @[ ], html)
|
|
|
|
proc newThreadForm(ctx: AppContext) {.async.} =
|
|
let html = buildHtml(tdiv):
|
|
form(`method`="POST", action=ctx.urlFor("submit")):
|
|
input(name="thread-title", `type`="text", placeholder="Title")
|
|
textarea(name="content")
|
|
input(`type`="submit", value="Post", class="submit-post")
|
|
resp base(ctx, "New Thread", @[ ], html)
|
|
|
|
proc submitPost(ctx: AppContext) {.async.} =
|
|
let title = ctx.getFormParams("thread-title")
|
|
if ctx.getFormParams("content") == "" or (title == "" and ctx.getFormParams("thread-id") == ""):
|
|
ctx.flash("Please actually have content.", FlashLevel.Warning)
|
|
resp redirect(ctx.request.headers["referer"], Http303)
|
|
return
|
|
if ctx.user.isSome:
|
|
ctx.db.transaction:
|
|
let threadID = if title != "": domain.postThread(ctx.db, title, ctx.user.get.id)
|
|
else: parseBiggestInt ctx.getFormParams("thread-id")
|
|
let content = ctx.getFormParams("content")
|
|
let newIndex = domain.postPost(ctx.db, content, threadID, ctx.user.get.id)
|
|
resp redirect(ctx.urlFor("thread", {"id": $threadID}) & "#post" & $newIndex, Http303)
|
|
else:
|
|
ctx.flash("Not logged in", FlashLevel.Warning)
|
|
resp redirect("/", Http303)
|
|
|
|
proc loginPage(ctx: AppContext) {.async.} =
|
|
let html = buildHtml(tdiv):
|
|
form(`method`="post", class="login"):
|
|
input(`type`="text", placeholder="Username", name="username")
|
|
input(`type`="password", placeholder="Password", name="password")
|
|
input(`type`="submit", class="login", value="Login", name="action")
|
|
details:
|
|
summary: text "Create new account"
|
|
input(`type`="password", placeholder="Confirm Password", name="confirm-password")
|
|
input(`type`="submit", class="create-account", value="Create Account", name="action")
|
|
resp base(ctx, "Login", @[ ], html)
|
|
|
|
proc doLogin(ctx: AppContext) {.async.} =
|
|
let username = ctx.getFormParams("username")
|
|
let password = ctx.getFormParams("password")
|
|
if username == "" or password == "":
|
|
ctx.flash("Password/username missing", FlashLevel.Error)
|
|
resp redirect(ctx.urlFor("login"), Http303)
|
|
return
|
|
let user = domain.getUser(ctx.db, username)
|
|
if ctx.getFormParams("action") == "Create Account":
|
|
if user.isSome:
|
|
ctx.flash("User already exists", FlashLevel.Error)
|
|
resp redirect(ctx.urlFor("login"), Http303)
|
|
else:
|
|
if password == ctx.getFormParams("confirm-password"):
|
|
let id = domain.addUser(ctx.db, username, password)
|
|
ctx.session["uid"] = $id
|
|
ctx.flash("Account created")
|
|
resp redirect(ctx.getQueryParams("redirect", "/"), Http303)
|
|
else:
|
|
ctx.flash("Passwords do not match", FlashLevel.Error)
|
|
resp redirect(ctx.urlFor("login"), Http303)
|
|
else:
|
|
if user.isSome:
|
|
if passwordVerify(password, user.get.pwhash):
|
|
ctx.session["uid"] = $user.get.id
|
|
ctx.flash("Logged in")
|
|
resp redirect(ctx.getQueryParams("redirect", "/"), Http303)
|
|
else:
|
|
ctx.flash("Password does not match", FlashLevel.Error)
|
|
resp redirect(ctx.urlFor("login"), Http303)
|
|
else:
|
|
ctx.flash("No such user", FlashLevel.Error)
|
|
resp redirect(ctx.urlFor("login"), Http303)
|
|
|
|
proc error500(ctx: AppContext) {.async.} =
|
|
logger().log(lvlError, getCurrentExceptionMsg())
|
|
resp getCurrentExceptionMsg(), Http500
|
|
|
|
var app = newApp(settings = settings)
|
|
app.use(@[staticFileMiddleware("static"), extendContextMiddleware(AppContext), dbMiddleware(), sessionMiddleware(settings, db), generalMiddleware()])
|
|
app.get("/", index, name="index")
|
|
app.get("/login", loginPage, name="login")
|
|
app.post("/login", doLogin, name="do-login")
|
|
app.get("/thread", newThreadForm, name="new-thread")
|
|
app.get("/thread/{id}", viewThread, name="thread")
|
|
app.post("/", submitPost, name="submit")
|
|
app.registerErrorHandler(Http500, error500)
|
|
app.run() |