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

View file

@ -13,6 +13,8 @@ import
import cgi except setCookie import cgi except setCookie
import options import options
import auth
import frontend/threadlist except User import frontend/threadlist except User
import frontend/[ import frontend/[
category, postlist, error, header, post, profile, user, karaxutils category, postlist, error, header, post, profile, user, karaxutils
@ -85,7 +87,6 @@ var
db: DbConn db: DbConn
isFTSAvailable: bool isFTSAvailable: bool
config: Config config: Config
useCaptcha: bool
captcha: ReCaptcha captcha: ReCaptcha
proc newForumError(message: string, proc newForumError(message: string,
@ -230,62 +231,7 @@ proc genGravatar(email: string, size: int = 80): string =
result = "<img width=\"$1\" height=\"$2\" src=\"$3\" />" % result = "<img width=\"$1\" height=\"$2\" src=\"$3\" />" %
[$size, $size, getGravatarUrl(email, size)] [$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: "") 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.} = proc resetPassword(c: TForumData, nick, antibot, userIp: string): Future[bool] {.async.} =
# captcha validation: # captcha validation:
if useCaptcha: if config.recaptchaSecretKey.len > 0:
var captchaValid: bool = false var captchaValid: bool = false
try: try:
captchaValid = await captcha.verify(antibot, userIp) captchaValid = await captcha.verify(antibot, userIp)
@ -365,15 +311,10 @@ proc checkLoggedIn(c: TForumData) =
c.req.ip, pass) c.req.ip, pass)
let row = getRow(db, 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.username = ||row[0]
c.email = ||row[1] c.email = ||row[1]
c.rank = parseEnum[Rank](||row[2]) 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 # Update lastOnline
db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?", db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?",
@ -917,16 +858,13 @@ proc initialise() =
database="nimforum") database="nimforum")
isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " &
"type='table' AND name='post_fts'")).len == 1 "type='table' AND name='post_fts'")).len == 1
config = loadConfig() config = loadConfig()
if len(config.recaptchaSecretKey) > 0 and len(config.recaptchaSiteKey) > 0: if len(config.recaptchaSecretKey) > 0 and len(config.recaptchaSiteKey) > 0:
useCaptcha = true
captcha = initReCaptcha(config.recaptchaSecretKey, config.recaptchaSiteKey) captcha = initReCaptcha(config.recaptchaSecretKey, config.recaptchaSiteKey)
else: else:
useCaptcha = false doAssert config.isDev, "Recaptcha required for production!"
var http = true echo("[WARNING] No recaptcha secret key specified.")
if paramCount() > 0:
if paramStr(1) == "scgi":
http = false
template createTFD() = template createTFD() =
var c {.inject.}: TForumData var c {.inject.}: TForumData
@ -1027,13 +965,13 @@ proc executeReply(c: TForumData, threadId: int, content: string,
# Verify that content can be parsed as RST. # Verify that content can be parsed as RST.
let retID = insertID( let retID = insertID(
db, db,
crud(crCreate, "post", "author", "ip", "header", "content", "thread"), crud(crCreate, "post", "author", "ip", "content", "thread"),
c.userId, c.req.ip, subject, content, $threadId, "" c.userId, c.req.ip, content, $threadId, ""
) )
discard tryExec( discard tryExec(
db, db,
crud(crCreate, "post_fts", "id", "header", "content"), crud(crCreate, "post_fts", "id", "content"),
retID.int, subject, content retID.int, content
) )
exec(db, sql"update thread set modified = DATETIME('now') where id = ?", 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"]) raise newForumError("Please choose a longer password", @["password"])
# captcha validation: # captcha validation:
if useCaptcha: if config.recaptchaSecretKey.len > 0:
var verifyFut = captcha.verify(antibot, userIp) var verifyFut = captcha.verify(antibot, userIp)
yield verifyFut yield verifyFut
if verifyFut.failed: if verifyFut.failed:
@ -1409,14 +1347,22 @@ routes:
post "/signup": post "/signup":
createTFD() createTFD()
let formData = request.formData let formData = request.formData
if not config.isDev:
cond "g-recaptcha-response" in formData
let username = formData["username"].body let username = formData["username"].body
let password = formData["password"].body let password = formData["password"].body
let recaptcha =
if "g-recaptcha-response" in formData:
formData["g-recaptcha-response"].body
else:
""
try: try:
discard await executeRegister( discard await executeRegister(
c, c,
username, username,
password, password,
formData["g-recaptcha-response"].body, recaptcha,
request.host, request.host,
formData["email"].body formData["email"].body
) )
@ -1446,7 +1392,7 @@ routes:
let status = UserStatus( let status = UserStatus(
user: user, user: user,
recaptchaSiteKey: recaptchaSiteKey:
if useCaptcha: if not config.isDev:
some(config.recaptchaSiteKey) some(config.recaptchaSiteKey)
else: else:
none[string]() none[string]()

View file

@ -75,8 +75,8 @@ when defined(js):
not getLoggedInUser().isNone not getLoggedInUser().isNone
proc renderHeader*(): VNode = proc renderHeader*(): VNode =
if state.data.isNone: if state.data.isNone and state.status == Http200:
getStatus() # TODO: Call this every render? getStatus()
let user = state.data.map(x => x.user).flatten let user = state.data.map(x => x.user).flatten
result = buildHtml(tdiv()): # TODO: Why do some buildHtml's need this? 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 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 <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 **shared and remixed**. In the future we may even provide all this data as a
convenient data dump. convenient data dump.
@ -16,13 +16,13 @@ attribution**::
Let us clarify what we mean by attribution. If you republish this content, we Let us clarify what we mean by attribution. If you republish this content, we
require that you: require that you:
* **Visually indicate that the content is from the Nimrod Forum**. It doesn’t * **Visually indicate that the content is from the Nim Forum**. It doesn’t
have to be obnoxious; a discreet text blurb is fine. have to be obnoxious; a discreet text blurb is fine.
* **Hyperlink directly to the original post** (e.g., * **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. * **Show the author names** for every post.
* **Hyperlink each author name** directly back to their user profile page * **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 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 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 heart’s content, as long as a good faith
effort is made to attribute the content! effort is made to attribute the content!
Content previous to the forum license change of 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. copyright, and therefore you cannot reuse it.

View file

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