Fixes forum to work in dev mode. Creates setup_nimforum script.

This commit is contained in:
Dominik Picheta 2018-05-17 19:22:46 +01:00
commit 7eb6b081ad
7 changed files with 259 additions and 216 deletions

View file

@ -1,128 +0,0 @@
#
#
# The Nim Forum
# (c) Copyright 2012 Andreas Rumpf, Dominik Picheta
# Look at license.txt for more info.
# All rights reserved.
#
import strutils, db_sqlite
var db = open(connection="nimforum.db", user="postgres", password="",
database="nimforum")
const
TUserName = "varchar(20)"
TPassword = "varchar(32)"
TEmail = "varchar(30)"
db.exec(sql"""
create table if not exists thread(
id integer primary key,
name varchar(100) not null,
views integer not null,
modified timestamp not null default (DATETIME('now'))
);""", [])
db.exec(sql"""
create unique index if not exists ThreadNameIx on thread (name);
""", [])
db.exec(sql("""
create table if not exists person(
id integer primary key,
name $# not null,
password $# not null,
email $# not null,
creation timestamp not null default (DATETIME('now')),
salt varbin(128) not null,
status varchar(30) not null,
lastOnline timestamp not null default (DATETIME('now'))
);""" % [TUserName, TPassword, TEmail]), [])
# echo "person table already exists"
db.exec(sql("""
alter table person
add ban varchar(128) not null default ''
"""))
db.exec(sql"""
create unique index if not exists UserNameIx on person (name);
""", [])
# ----------------------- Forum ------------------------------------------------
if not db.tryExec(sql"""
create table if not exists post(
id integer primary key,
author integer not null,
ip inet not null,
header varchar(100) not null,
content varchar(1000) not null,
thread integer not null,
creation timestamp not null default (DATETIME('now')),
foreign key (thread) references thread(id),
foreign key (author) references person(id)
);""", []):
echo "post table already exists"
# -------------------- Session -------------------------------------------------
if not db.tryExec(sql("""
create table if not exists session(
id integer primary key,
ip inet not null,
password $# not null,
userid integer not null,
lastModified timestamp not null default (DATETIME('now')),
foreign key (userid) references person(id)
);""" % [TPassword]), []):
echo "session table already exists"
if not db.tryExec(sql"""
create table if not exists antibot(
id integer primary key,
ip inet not null,
answer varchar(30) not null,
created timestamp not null default (DATETIME('now'))
);""", []):
echo "antibot table already exists"
db.exec sql"create index PersonStatusIdx on person(status);"
db.exec sql"create index PostByAuthorIdx on post(thread, author);"
# -------------------- Search --------------------------------------------------
if not db.tryExec(sql"""
CREATE VIRTUAL TABLE thread_fts USING fts4 (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL
);""", []):
echo "thread_fts table already exists or fts4 not supported"
else:
db.exec(sql"""
INSERT INTO thread_fts
SELECT id, name FROM thread;
""", [])
if not db.tryExec(sql"""
CREATE VIRTUAL TABLE post_fts USING fts4 (
id INTEGER PRIMARY KEY,
header VARCHAR(100) NOT NULL,
content VARCHAR(1000) NOT NULL
);""", []):
echo "post_fts table already exists or fts4 not supported"
else:
db.exec(sql"""
INSERT INTO post_fts
SELECT id, header, content FROM post;
""", [])
# ------------------------------------------------------------------------------
#discard stdin.readline()
close(db)

View file

@ -320,12 +320,9 @@
<td>${fieldValid(c, "email", "E-Mail:")}</td>
<td>${textWidget(c, "email", reuseText, maxlength=300)}</td>
</tr>
#if useCaptcha:
<tr>
<td>${fieldValid(c, "g-recaptcha-response", "Captcha:")}</td>
<td>${captcha.render(includeNoScript=true)}</td>
</tr>
#end if
</table>
#if c.errorMsg != "":
<div style="float: left; width: 100%;">
@ -494,12 +491,10 @@
<td>${fieldValid(c, "nick", "Your nickname:")}</td>
<td><input type="text" name="nick" maxlength="20" /></td>
</tr>
#if useCaptcha:
<tr>
<td>${fieldValid(c, "g-recaptcha-response", "Captcha:")}</td>
<td>${captcha.render(includeNoScript=true)}</td>
</tr>
#end if
</table>
#if c.errorMsg != "":
<div style="float: left; width: 100%;">

View file

@ -13,6 +13,8 @@ import
import cgi except setCookie
import options
import auth
import frontend/threadlist except User
import frontend/[
category, postlist, error, header, post, profile, user, karaxutils
@ -85,7 +87,6 @@ var
db: DbConn
isFTSAvailable: bool
config: Config
useCaptcha: bool
captcha: ReCaptcha
proc newForumError(message: string,
@ -230,62 +231,7 @@ proc genGravatar(email: string, size: int = 80): string =
result = "<img width=\"$1\" height=\"$2\" src=\"$3\" />" %
[$size, $size, getGravatarUrl(email, size)]
proc randomSalt(): string =
result = ""
for i in 0..127:
var r = random(225)
if r >= 32 and r <= 126:
result.add(chr(random(225)))
proc devRandomSalt(): string =
when defined(posix):
result = ""
var f = open("/dev/urandom")
var randomBytes: array[0..127, char]
discard f.readBuffer(addr(randomBytes), 128)
for i in 0..127:
if ord(randomBytes[i]) >= 32 and ord(randomBytes[i]) <= 126:
result.add(randomBytes[i])
f.close()
else:
result = randomSalt()
proc makeSalt(): string =
## Creates a salt using a cryptographically secure random number generator.
##
## Ensures that the resulting salt contains no ``\0``.
try:
result = devRandomSalt()
except IOError:
result = randomSalt()
var newResult = ""
for i in 0 .. <result.len:
if result[i] != '\0':
newResult.add result[i]
return newResult
proc makePassword(password, salt: string, comparingTo = ""): string =
## Creates an MD5 hash by combining password and salt.
when defined(windows):
result = getMD5(salt & getMD5(password))
else:
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
result = hash(getMD5(salt & getMD5(password)), bcryptSalt)
proc makeIdentHash(user, password, epoch, secret: string,
comparingTo = ""): string =
## Creates a hash verifying the identity of a user. Used for password reset
## links and email activation links.
## If ``epoch`` is smaller than the epoch of the user's last login then
## the link is invalid.
## The ``secret`` is the 'salt' field in the ``person`` table.
echo(user, password, epoch, secret)
when defined(windows):
result = getMD5(user & password & epoch & secret)
else:
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
result = hash(user & password & epoch & secret, bcryptSalt)
# -----------------------------------------------------------------------------
template `||`(x: untyped): untyped = (if not isNil(x): x else: "")
@ -301,7 +247,7 @@ proc setError(c: TForumData, field, msg: string): bool {.inline.} =
proc resetPassword(c: TForumData, nick, antibot, userIp: string): Future[bool] {.async.} =
# captcha validation:
if useCaptcha:
if config.recaptchaSecretKey.len > 0:
var captchaValid: bool = false
try:
captchaValid = await captcha.verify(antibot, userIp)
@ -365,15 +311,10 @@ proc checkLoggedIn(c: TForumData) =
c.req.ip, pass)
let row = getRow(db,
sql"select name, email, status, ban from person where id = ?", c.userid)
sql"select name, email, status from person where id = ?", c.userid)
c.username = ||row[0]
c.email = ||row[1]
c.rank = parseEnum[Rank](||row[2])
let ban = getBanErrorMsg(||row[3], c.rank)
if ban.len > 0:
discard c.setError("name", ban)
logout(c)
return
# Update lastOnline
db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?",
@ -917,16 +858,13 @@ proc initialise() =
database="nimforum")
isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " &
"type='table' AND name='post_fts'")).len == 1
config = loadConfig()
if len(config.recaptchaSecretKey) > 0 and len(config.recaptchaSiteKey) > 0:
useCaptcha = true
captcha = initReCaptcha(config.recaptchaSecretKey, config.recaptchaSiteKey)
else:
useCaptcha = false
var http = true
if paramCount() > 0:
if paramStr(1) == "scgi":
http = false
doAssert config.isDev, "Recaptcha required for production!"
echo("[WARNING] No recaptcha secret key specified.")
template createTFD() =
var c {.inject.}: TForumData
@ -1027,13 +965,13 @@ proc executeReply(c: TForumData, threadId: int, content: string,
# Verify that content can be parsed as RST.
let retID = insertID(
db,
crud(crCreate, "post", "author", "ip", "header", "content", "thread"),
c.userId, c.req.ip, subject, content, $threadId, ""
crud(crCreate, "post", "author", "ip", "content", "thread"),
c.userId, c.req.ip, content, $threadId, ""
)
discard tryExec(
db,
crud(crCreate, "post_fts", "id", "header", "content"),
retID.int, subject, content
crud(crCreate, "post_fts", "id", "content"),
retID.int, content
)
exec(db, sql"update thread set modified = DATETIME('now') where id = ?",
@ -1156,7 +1094,7 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp,
raise newForumError("Please choose a longer password", @["password"])
# captcha validation:
if useCaptcha:
if config.recaptchaSecretKey.len > 0:
var verifyFut = captcha.verify(antibot, userIp)
yield verifyFut
if verifyFut.failed:
@ -1409,14 +1347,22 @@ routes:
post "/signup":
createTFD()
let formData = request.formData
if not config.isDev:
cond "g-recaptcha-response" in formData
let username = formData["username"].body
let password = formData["password"].body
let recaptcha =
if "g-recaptcha-response" in formData:
formData["g-recaptcha-response"].body
else:
""
try:
discard await executeRegister(
c,
username,
password,
formData["g-recaptcha-response"].body,
recaptcha,
request.host,
formData["email"].body
)
@ -1446,7 +1392,7 @@ routes:
let status = UserStatus(
user: user,
recaptchaSiteKey:
if useCaptcha:
if not config.isDev:
some(config.recaptchaSiteKey)
else:
none[string]()

View file

@ -75,8 +75,8 @@ when defined(js):
not getLoggedInUser().isNone
proc renderHeader*(): VNode =
if state.data.isNone:
getStatus() # TODO: Call this every render?
if state.data.isNone and state.status == Http200:
getStatus()
let user = state.data.map(x => x.user).flatten
result = buildHtml(tdiv()): # TODO: Why do some buildHtml's need this?

228
setup_nimforum.nim Normal file
View file

@ -0,0 +1,228 @@
#
#
# The Nim Forum
# (c) Copyright 2018 Andreas Rumpf, Dominik Picheta
# Look at license.txt for more info.
# All rights reserved.
#
# Script to initialise the nimforum.
import strutils, db_sqlite, os, times, json
import auth, frontend/user
proc backup(path: string) =
if existsFile(path):
let backupPath = path & "." & $getTime().toUnix()
echo(path, " already exists. Moving to ", backupPath)
moveFile(path, backupPath)
proc initialiseDb(admin: tuple[username, password, email: string]) =
let path = getCurrentDir() / "nimforum.db"
backup(path)
var db = open(connection="nimforum.db", user="", password="",
database="nimforum")
const
userNameType = "varchar(20)"
passwordType = "varchar(50)"
emailType = "varchar(254)" # https://stackoverflow.com/a/574698/492186
# -- Category
db.exec(sql"""
create table category(
id integer primary key,
name varchar(100) not null,
description varchar(500) not null,
color varchar(10) not null
);
insert into category (id, name, description, color)
values (0, 'Default', '', '');
""")
# -- Thread
db.exec(sql"""
create table thread(
id integer primary key,
name varchar(100) not null,
views integer not null,
modified timestamp not null default (DATETIME('now')),
category integer not null default 0,
isLocked boolean not null default 0,
solution integer,
isDeleted boolean not null default 0,
foreign key (category) references category(id),
foreign key (solution) references post(id)
);""", [])
db.exec(sql"""
create unique index ThreadNameIx on thread (name);
""", [])
# -- Person
db.exec(sql("""
create table person(
id integer primary key,
name $# not null,
password $# not null,
email $# not null,
creation timestamp not null default (DATETIME('now')),
salt varbin(128) not null,
status varchar(30) not null,
lastOnline timestamp not null default (DATETIME('now')),
isDeleted boolean not null default 0
);""" % [userNameType, passwordType, emailType]), [])
db.exec(sql"""
create unique index UserNameIx on person (name);
""", [])
db.exec sql"create index PersonStatusIdx on person(status);"
# Create default user.
let salt = makeSalt()
let password = makePassword(admin.password, salt)
db.exec(sql"""
insert into person (id, name, password, email, salt, status)
values (0, ?, ?, ?, ?, ?);
""", admin.username, password, admin.email, salt, $Admin)
# -- Post
db.exec(sql"""
create table post(
id integer primary key,
author integer not null,
ip inet not null,
content varchar(1000) not null,
thread integer not null,
creation timestamp not null default (DATETIME('now')),
isDeleted boolean not null default 0,
foreign key (thread) references thread(id),
foreign key (author) references person(id)
);""", [])
db.exec sql"create index PostByAuthorIdx on post(thread, author);"
db.exec(sql"""
create table postRevision(
id integer primary key,
creation timestamp not null default (DATETIME('now')),
original integer not null,
content varchar(1000) not null,
foreign key (original) references post(id)
)
""")
# -- Session
db.exec(sql("""
create table session(
id integer primary key,
ip inet not null,
password $# not null,
userid integer not null,
lastModified timestamp not null default (DATETIME('now')),
foreign key (userid) references person(id)
);""" % [passwordType]), [])
# -- Likes
db.exec(sql("""
create table like(
id integer primary key,
author integer not null,
post integer not null,
creation timestamp not null default (DATETIME('now')),
foreign key (author) references person(id),
foreign key (post) references post(id)
)
"""))
# -- Report
db.exec(sql("""
create table report(
id integer primary key,
author integer not null,
post integer not null,
kind varchar(30) not null,
content varchar(500) not null default '',
foreign key (author) references person(id),
foreign key (post) references post(id)
)
"""))
# -- FTS
if not db.tryExec(sql"""
CREATE VIRTUAL TABLE thread_fts USING fts4 (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL
);""", []):
echo "thread_fts table already exists or fts4 not supported"
else:
db.exec(sql"""
INSERT INTO thread_fts
SELECT id, name FROM thread;
""", [])
if not db.tryExec(sql"""
CREATE VIRTUAL TABLE post_fts USING fts4 (
id INTEGER PRIMARY KEY,
content VARCHAR(1000) NOT NULL
);""", []):
echo "post_fts table already exists or fts4 not supported"
else:
db.exec(sql"""
INSERT INTO post_fts
SELECT id, content FROM post;
""", [])
close(db)
proc initialiseConfig(
name, hostname: string,
recaptcha: tuple[siteKey, secretKey: string],
smtp: tuple[address, user, password: string],
isDev: bool
) =
let path = getCurrentDir() / "forum.json"
backup(path)
var j = %{
"name": %name,
"hostname": %hostname,
"recaptchaSiteKey": %recaptcha.siteKey,
"recaptchaSecretKey": %recaptcha.secretKey,
"smtpAddress": %smtp.address,
"smtpUser": %smtp.user,
"smtpPassword": %smtp.password,
"isDev": %isDev
}
writeFile(path, $j)
when isMainModule:
if paramCount() > 0 and paramStr(1) == "--dev":
echo("Initialising nimforum for development...")
initialiseConfig(
"Development Forum",
"localhost.local",
recaptcha=("", ""),
smtp=("", "", ""),
isDev=true
)
initialiseDb(
admin=("admin", "admin", "admin@localhost.local")
)

View file

@ -1,7 +1,7 @@
Forum content license
=====================
All the content contributed to the Nimrod Forum is `cc-wiki (aka cc-by-sa)
All the content contributed to the Nim Forum is `cc-wiki (aka cc-by-sa)
<http://creativecommons.org/licenses/by-sa/3.0/>`_ licensed, intended to be
**shared and remixed**. In the future we may even provide all this data as a
convenient data dump.
@ -16,13 +16,13 @@ attribution**::
Let us clarify what we mean by attribution. If you republish this content, we
require that you:
* **Visually indicate that the content is from the Nimrod Forum**. It doesnt
* **Visually indicate that the content is from the Nim Forum**. It doesnt
have to be obnoxious; a discreet text blurb is fine.
* **Hyperlink directly to the original post** (e.g.,
http://forum.nimrod-lang.org/t/186)
http://forum.nim-lang.org/t/186)
* **Show the author names** for every post.
* **Hyperlink each author name** directly back to their user profile page
(e.g., http://forum.nimrod-lang.org/profile/Araq)
(e.g., http://forum.nim-lang.org/profile/Araq)
By “directly”, we mean each hyperlink must point directly to our domain in
standard HTML visible even with JavaScript disabled, and not use a tinyurl or
@ -38,5 +38,5 @@ Feel free to remix and reuse to your hearts content, as long as a good faith
effort is made to attribute the content!
Content previous to the forum license change of
http://forum.nimrod-lang.org/t/186 remains under the original authors'
http://forum.nim-lang.org/t/186 remains under the original authors'
copyright, and therefore you cannot reuse it.

View file

@ -24,6 +24,7 @@ type
mlistAddress: string
recaptchaSecretKey*: string
recaptchaSiteKey*: string
isDev*: bool
var docConfig: StringTableRef
@ -43,6 +44,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config =
result.mlistAddress = root{"mlistAddress"}.getStr("")
result.recaptchaSecretKey = root{"recaptchaSecretKey"}.getStr("")
result.recaptchaSiteKey = root{"recaptchaSiteKey"}.getStr("")
result.isDev = root{"isDev"}.getBool()
except:
echo("[WARNING] Couldn't read config file: ", filename)