new Rank enum; admins and moderators; sane data model

This commit is contained in:
Araq 2017-01-01 20:34:25 +01:00
commit aff6e426eb
6 changed files with 148 additions and 114 deletions

View file

@ -36,7 +36,7 @@ create table if not exists person(
email $# not null, email $# not null,
creation timestamp not null default (DATETIME('now')), creation timestamp not null default (DATETIME('now')),
salt varbin(128) not null, salt varbin(128) not null,
status integer not null, status varchar(30) not null,
admin bool default false, admin bool default false,
lastOnline timestamp not null default (DATETIME('now')) lastOnline timestamp not null default (DATETIME('now'))
);""" % [TUserName, TPassword, TEmail]), []) );""" % [TUserName, TPassword, TEmail]), [])

View file

@ -1,11 +1,12 @@
import strutils, db_sqlite import strutils, db_sqlite, ranks
var db = Open(connection="nimforum.db", user="postgres", password="", var db = open(connection="nimforum.db", user="postgres", password="",
database="nimforum") database="nimforum")
db.exec(sql"""ALTER TABLE person add column db.exec(sql("update person set status = ?"), $User)
lastOnline timestamp db.exec(sql("update person set status = ? where ban <> ''"), $Troll)
""", []) db.exec(sql("update person set status = ? where ban like '%spam%'"), $Spammer)
db.exec(sql("update person set status = ? where admin"), $Admin)
close(db) close(db)

View file

@ -6,7 +6,10 @@
# #
# #
#proc genThreadsList(c: var TForumData, count: var int): string = #proc genThreadsList(c: var TForumData, count: var int): string =
# const query = sql"select id, name, views, modified from thread order by modified desc limit ?, ?" # const query = sql"""select id, name, views, modified from thread
# where id in (select thread from post where author in
# (select id from person where ban <> 'MODERATED'))
# order by modified desc limit ?, ?"""
# const threadId = 0 # const threadId = 0
# const name = 1 # const name = 1
# const views = 2 # const views = 2
@ -113,7 +116,9 @@
# #
#proc genPostsList(c: var TForumData, threadId: string, count: var int): string = #proc genPostsList(c: var TForumData, threadId: string, count: var int): string =
# const query = sql"""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, # const query = sql"""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p,
# person u where u.id = p.author and p.thread = ? order by p.id limit ?, ?""" # person u
# where u.id = p.author and p.thread = ? and p.ban <> 'MODERATED'
# order by p.id limit ?, ?"""
# const postId = 0 # const postId = 0
# const userName = 1 # const userName = 1
# const postHeader = 2 # const postHeader = 2
@ -146,7 +151,7 @@
<a class="name" href="$profileUrl">${xmlEncode(%userName)}</a> <a class="name" href="$profileUrl">${xmlEncode(%userName)}</a>
#if c.userId == %postAuthor and c.currentPost.subject.len == 0: #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<hr/><a href="${c.genThreadUrl(%postId, "edit")}">Edit post</a> <hr/><a href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
#elif c.isAdmin and c.currentPost.subject.len == 0: #elif c.rank >= Moderator and c.currentPost.subject.len == 0:
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit")}">Edit post</a> <hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
#end if #end if
</div> </div>
@ -184,15 +189,15 @@
</div> </div>
<form action="${c.req.makeUri(action, false) & "#preview"}" method="POST"> <form action="${c.req.makeUri(action, false) & "#preview"}" method="POST">
#if action == "doreply": #if action == "doreply":
${HiddenField(c, "subject", title)} ${hiddenField(c, "subject", title)}
#else: #else:
${FieldValid(c, "subject", "Subject:")} ${fieldValid(c, "subject", "Subject:")}
${TextWidget(c, "subject", title, maxlength=100)} ${textWidget(c, "subject", title, maxlength=100)}
<br /> <br />
#end if #end if
${FieldValid(c, "content", "Content:")}<br /> ${fieldValid(c, "content", "Content:")}<br />
${TextAreaWidget(c, "content", content)}<br /> ${textAreaWidget(c, "content", content)}<br />
${FormSession(c, action)} ${formSession(c, action)}
# if isEdit: # if isEdit:
<input type="checkbox" name="delete" value="Delete">Delete Post<br /> <input type="checkbox" name="delete" value="Delete">Delete Post<br />
@ -226,20 +231,20 @@
<form action="${c.req.makeUri("/doregister", false)}" method="POST"> <form action="${c.req.makeUri("/doregister", false)}" method="POST">
<table border="0"> <table border="0">
<tr> <tr>
<td>${FieldValid(c, "name", "Username:")}</td> <td>${fieldValid(c, "name", "Username:")}</td>
<td>${TextWidget(c, "name", reuseText, maxlength=20)}</td> <td>${textWidget(c, "name", reuseText, maxlength=20)}</td>
</tr> </tr>
<tr> <tr>
<td>${FieldValid(c, "new_password", "Password:")}</td> <td>${fieldValid(c, "new_password", "Password:")}</td>
<td><input type="password" name="new_password" /></td> <td><input type="password" name="new_password" /></td>
</tr> </tr>
<tr> <tr>
<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>
<tr> <tr>
<td>${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td> <td>${fieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td>
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td> <td>${textWidget(c, "antibot", "", maxlength=4)}</td>
</tr> </tr>
</table> </table>
#if c.errorMsg != "": #if c.errorMsg != "":
@ -288,7 +293,7 @@
# #
# #
#proc genSearchResults(c: var TForumData, #proc genSearchResults(c: var TForumData,
# results: iterator: db_sqlite.Row {.closure, tags: [FReadDB].}, # results: iterator: db_sqlite.Row {.closure, tags: [ReadDbEffect].},
# count: var int): string = # count: var int): string =
# const threadId = 0 # const threadId = 0
# const threadName = 1 # const threadName = 1
@ -326,7 +331,7 @@
<div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(%userName)}</a></div> <div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(%userName)}</a></div>
#if c.userId == %postAuthor and c.currentPost.subject.len == 0: #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<hr/><a href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a> <hr/><a href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
#elif c.isAdmin and c.currentPost.subject.len == 0: #elif c.rank >= Moderator and c.currentPost.subject.len == 0:
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a> <hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
#end if #end if
</div> </div>
@ -383,12 +388,12 @@
<form action="${c.req.makeUri("/doresetpassword", false)}" method="POST"> <form action="${c.req.makeUri("/doresetpassword", false)}" method="POST">
<table border="0"> <table border="0">
<tr> <tr>
<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>
<tr> <tr>
<td>${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td> <td>${fieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td>
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td> <td>${textWidget(c, "antibot", "", maxlength=4)}</td>
</tr> </tr>
</table> </table>
#if c.errorMsg != "": #if c.errorMsg != "":

173
forum.nim
View file

@ -7,9 +7,9 @@
# #
import import
os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, os, strutils, times, md5, strtabs, cgi, math, db_sqlite,
captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
parseutils, utils, random, rst parseutils, utils, random, rst, ranks
when not defined(windows): when not defined(windows):
import bcrypt # TODO import bcrypt # TODO
@ -25,8 +25,6 @@ const
MaxPagesFromCurrent = 8 MaxPagesFromCurrent = 8
noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"] noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"]
noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"] noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"]
banReasonDeactivated = "DEACTIVATED"
banReasonEmailUnconfirmed = "EMAILCONFIRMATION"
type type
TCrud = enum crCreate, crRead, crUpdate, crDelete TCrud = enum crCreate, crRead, crUpdate, crDelete
@ -35,7 +33,7 @@ type
threadid: int threadid: int
postid: int postid: int
userName, userPass, email: string userName, userPass, email: string
isAdmin: bool rank: Rank
TPost = tuple[subject, content: string] TPost = tuple[subject, content: string]
@ -70,6 +68,7 @@ type
lastOnline: int lastOnline: int
email: string email: string
ban: string ban: string
rank: Rank
ForumError = object of Exception ForumError = object of Exception
@ -103,27 +102,27 @@ proc loggedIn(c: TForumData): bool =
const const
reuseText = "\1" reuseText = "\1"
proc TextWidget(c: TForumData, name, defaultText: string, proc textWidget(c: TForumData, name, defaultText: string,
maxlength = 30, size = -1): string = maxlength = 30, size = -1): string =
let x = if defaultText != reuseText: defaultText let x = if defaultText != reuseText: defaultText
else: xmlEncode(c.req.params.getOrDefault(name)) else: xmlEncode(c.req.params.getOrDefault(name))
return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [ return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [
name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""] name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""]
proc HiddenField(c: TForumData, name, defaultText: string): string = proc hiddenField(c: TForumData, name, defaultText: string): string =
let x = xmlencode( let x = xmlencode(
if defaultText != reuseText: defaultText if defaultText != reuseText: defaultText
else: c.req.params.getOrDefault(name) else: c.req.params.getOrDefault(name)
) )
return """<input type="hidden" name="$1" value="$2"/>""" % [name, x] return """<input type="hidden" name="$1" value="$2"/>""" % [name, x]
proc TextAreaWidget(c: TForumData, name, defaultText: string): string = proc textAreaWidget(c: TForumData, name, defaultText: string): string =
let x = if defaultText != reuseText: defaultText let x = if defaultText != reuseText: defaultText
else: xmlEncode(c.req.params.getOrDefault(name)) else: xmlEncode(c.req.params.getOrDefault(name))
return """<textarea name="$1">$2</textarea>""" % [ return """<textarea name="$1">$2</textarea>""" % [
name, x] name, x]
proc FieldValid(c: TForumData, name, text: string): string = proc fieldValid(c: TForumData, name, text: string): string =
if name == c.invalidField: if name == c.invalidField:
result = """<span style="color:red">$1</span>""" % text result = """<span style="color:red">$1</span>""" % text
else: else:
@ -141,12 +140,12 @@ proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNu
result.add("#" & postId) result.add("#" & postId)
result = c.req.makeUri(result, absolute = false) result = c.req.makeUri(result, absolute = false)
proc FormSession(c: var TForumData, nextAction: string): string = proc formSession(c: var TForumData, nextAction: string): string =
return """<input type="hidden" name="threadid" value="$1" /> return """<input type="hidden" name="threadid" value="$1" />
<input type="hidden" name="postid" value="$2" />""" % [ <input type="hidden" name="postid" value="$2" />""" % [
$c.threadId, $c.postid] $c.threadId, $c.postid]
proc UrlButton(c: var TForumData, text, url: string): string = proc urlButton(c: var TForumData, text, url: string): string =
return ("""<a class="url_button" href="$1">$2</a>""") % [ return ("""<a class="url_button" href="$1">$2</a>""") % [
url, text] url, text]
@ -343,10 +342,10 @@ proc register(c: var TForumData, name, pass, antibot,
# add account to person table # add account to person table
exec(db, exec(db,
sql("INSERT INTO person(name, password, email, salt, status, lastOnline, " & sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " &
"ban) VALUES (?, ?, ?, ?, 'user', DATETIME('now'), ?)"), name, "VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name,
password, email, salt, password, email, salt,
when defined(dev): "" else: banReasonEmailUnconfirmed) when defined(dev): $User else: $EmailUnconfirmed)
return true return true
@ -384,15 +383,19 @@ proc logout(c: var TForumData) =
c.userpass = "" c.userpass = ""
exec(db, query, c.req.ip, c.req.cookies["sid"]) exec(db, query, c.req.ip, c.req.cookies["sid"])
proc getBanErrorMsg(banValue: string): string = proc getBanErrorMsg(banValue: string; rank: Rank): string =
case banValue if banValue.len > 0:
of "": return ""
of banReasonDeactivated:
return "Your account has been deactivated."
of banReasonEmailUnconfirmed:
return "You need to confirm your email first."
else:
return "You have been banned: " & banValue return "You have been banned: " & banValue
case rank
of Spammer: return "You are a spammer."
of Troll: return "You are a troll."
of Inactive: return "Your account has been deactivated."
of EmailUnconfirmed:
return "You need to confirm your email first."
of Moderated:
return "Your posts await moderation."
of User, Moderator, Admin:
return ""
proc checkLoggedIn(c: var TForumData) = proc checkLoggedIn(c: var TForumData) =
if not c.req.cookies.hasKey("sid"): return if not c.req.cookies.hasKey("sid"): return
@ -407,14 +410,13 @@ proc checkLoggedIn(c: var TForumData) =
c.req.ip, pass) c.req.ip, pass)
let row = getRow(db, let row = getRow(db,
sql"select name, email, admin, ban from person where id = ?", c.userid) sql"select name, email, status, ban from person where id = ?", c.userid)
c.username = ||row[0] c.username = ||row[0]
c.email = ||row[1] c.email = ||row[1]
c.isAdmin = parseBool(||row[2]) c.rank = parseEnum[Rank](||row[2])
# Check ban status. let ban = getBanErrorMsg(||row[3], c.rank)
let banErrorMsg = getBanErrorMsg(||row[3]) if ban.len > 0:
if banErrorMsg.len > 0: discard c.setError("name", ban)
discard c.setError("name", banErrorMsg)
logout(c) logout(c)
return return
@ -494,7 +496,7 @@ template checkLogin(c: untyped) =
if not loggedIn(c): return setError(c, "", "User is not logged in") if not loggedIn(c): return setError(c, "", "User is not logged in")
template checkOwnership(c, postId: untyped) = template checkOwnership(c, postId: untyped) =
if not c.isAdmin: if c.rank < Moderator:
let x = getValue(db, sql"select author from post where id = ?", let x = getValue(db, sql"select author from post where id = ?",
postId) postId)
if x != c.userId: if x != c.userId:
@ -617,6 +619,13 @@ proc rateLimitCheck(c: var TForumData): bool =
proc makeThreadURL(c: var TForumData): string = proc makeThreadURL(c: var TForumData): string =
c.req.makeUri("/t/" & $c.threadId) c.req.makeUri("/t/" & $c.threadId)
template postChecks() {.dirty.} =
if spamCheck(c, subject, content):
echo("[WARNING] Found spam: ", subject)
return true
if rateLimitCheck(c):
return setError(c, "subject", "You're posting too fast.")
proc reply(c: var TForumData): bool = proc reply(c: var TForumData): bool =
# reply to an existing thread # reply to an existing thread
checkLogin(c) checkLogin(c)
@ -624,17 +633,15 @@ proc reply(c: var TForumData): bool =
if c.isPreview: if c.isPreview:
setPreviewData(c) setPreviewData(c)
else: else:
if spamCheck(c, subject, content): postChecks()
echo("[WARNING] Found spam: ", subject)
return true
if rateLimitCheck(c):
return setError(c, "subject", "You're posting too fast.")
writeToDb(c, crCreate, true) writeToDb(c, crCreate, true)
exec(db, sql"update thread set modified = DATETIME('now') where id = ?", exec(db, sql"update thread set modified = DATETIME('now') where id = ?",
$c.threadId) $c.threadId)
asyncCheck sendMailToMailingList(c.config, c.username, c.email, if c.rank >= User:
subject, content, threadId=c.threadId, postId=c.postID, is_reply=true, threadUrl=c.makeThreadURL()) asyncCheck sendMailToMailingList(c.config, c.username, c.email,
subject, content, threadId=c.threadId, postId=c.postID, is_reply=true,
threadUrl=c.makeThreadURL())
result = true result = true
proc newThread(c: var TForumData): bool = proc newThread(c: var TForumData): bool =
@ -646,11 +653,7 @@ proc newThread(c: var TForumData): bool =
setPreviewData(c) setPreviewData(c)
c.threadID = transientThread c.threadID = transientThread
else: else:
if spamCheck(c, subject, content): postChecks()
echo("[WARNING] Found spam: ", subject)
return true
if rateLimitCheck(c):
return setError(c, "subject", "You're posting too fast.")
c.threadID = tryInsertID(db, query, c.req.params["subject"]).int c.threadID = tryInsertID(db, query, c.req.params["subject"]).int
if c.threadID < 0: return setError(c, "subject", "Subject already exists") if c.threadID < 0: return setError(c, "subject", "Subject already exists")
discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"), discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"),
@ -658,26 +661,29 @@ proc newThread(c: var TForumData): bool =
writeToDb(c, crCreate, false) writeToDb(c, crCreate, false)
discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')")
discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')")
asyncCheck sendMailToMailingList(c.config, c.username, c.email, if c.rank >= User:
subject, content, threadId=c.threadID, postId=c.postID, is_reply=false, threadUrl=c.makeThreadURL()) asyncCheck sendMailToMailingList(c.config, c.username, c.email,
subject, content, threadId=c.threadID, postId=c.postID, is_reply=false,
threadUrl=c.makeThreadURL())
result = true result = true
proc login(c: var TForumData, name, pass: string): bool = proc login(c: var TForumData, name, pass: string): bool =
# get form data: # get form data:
const query = const query =
sql"select id, name, password, email, salt, admin, ban from person where name = ?" sql"select id, name, password, email, salt, status, ban from person where name = ?"
if name.len == 0: if name.len == 0:
return c.setError("name", "Username cannot be nil.") return c.setError("name", "Username cannot be nil.")
var success = false var success = false
for row in fastRows(db, query, name): for row in fastRows(db, query, name):
if row[2] == makePassword(pass, row[4], row[2]): if row[2] == makePassword(pass, row[4], row[2]):
if row[6].len > 0: c.rank = parseEnum[Rank](row[5])
return c.setError("name", getBanErrorMsg(row[6])) let ban = getBanErrorMsg(row[6], c.rank)
if ban.len > 0:
return c.setError("name", ban)
c.userid = row[0] c.userid = row[0]
c.username = row[1] c.username = row[1]
c.userpass = row[2] c.userpass = row[2]
c.email = row[3] c.email = row[3]
c.isAdmin = row[5].parseBool
success = true success = true
break break
if success: if success:
@ -705,6 +711,11 @@ proc setBan(c: var TForumData, nick, reason: string): bool =
sql("update person set ban = ? where name = ?") sql("update person set ban = ? where name = ?")
return tryExec(db, query, reason, nick) return tryExec(db, query, reason, nick)
proc setStatus(c: var TForumData, nick: string, status: Rank): bool =
const query =
sql("update person set status = ? where name = ?")
return tryExec(db, query, $status, nick)
proc deleteAll(c: var TForumData, nick: string): bool = proc deleteAll(c: var TForumData, nick: string): bool =
const query = const query =
sql("delete from post where author = (select id from person where name = ?)") sql("delete from post where author = (select id from person where name = ?)")
@ -874,7 +885,7 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool =
if uid == "": return false if uid == "": return false
result = true result = true
const totalPostsQuery = const totalPostsQuery =
sql"SELECT count(*) FROM post WHERE author = ?" sql"select count(*) from post where author = ?"
ui.posts = getValue(db, totalPostsQuery, uid).parseInt ui.posts = getValue(db, totalPostsQuery, uid).parseInt
const totalThreadsQuery = const totalThreadsQuery =
sql("select count(*) from thread where id in (select thread from post where" & sql("select count(*) from thread where id in (select thread from post where" &
@ -882,11 +893,13 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool =
ui.threads = getValue(db, totalThreadsQuery, uid).parseInt ui.threads = getValue(db, totalThreadsQuery, uid).parseInt
const lastOnlineQuery = const lastOnlineQuery =
sql"select strftime('%s', lastOnline) from person where id = ?" sql"""select strftime('%s', lastOnline), email, ban, status
let lastOnlineDBVal = getValue(db, lastOnlineQuery, uid) from person where id = ?"""
ui.lastOnline = if lastOnlineDBVal != "": lastOnlineDBVal.parseInt else: -1 let row = db.getRow lastOnlineQuery
ui.email = getValue(db, sql"select email from person where id = ?", uid) ui.lastOnline = if row[0].len > 0: row[0].parseInt else: -1
ui.ban = getValue(db, sql"select ban from person where id = ?", uid) ui.email = row[1]
ui.ban = row[2]
ui.rank = parseEnum[Rank](row[3])
proc genSetUserStatusUrl(c: var TForumData, nick: string, typ: string): string = proc genSetUserStatusUrl(c: var TForumData, nick: string, typ: string): string =
c.req.makeUri("/setUserStatus?nick=$1&type=$2" % [nick, typ]) c.req.makeUri("/setUserStatus?nick=$1&type=$2" % [nick, typ])
@ -930,21 +943,12 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string =
), ),
tr( tr(
th("Status"), th("Status"),
td(case ui.ban td($ui.rank)
of banReasonDeactivated:
"Deactivated"
of banReasonEmailUnconfirmed:
"Awaiting email confirmation"
of "":
"Active"
else:
"Banned: " & ui.ban
)
), ),
tr( tr(
th(""), th(""),
td(if c.isAdmin and ui.ban != banReasonDeactivated: td(if c.rank >= Moderator and c.rank > ui.rank:
if ui.ban == "": if ui.rank >= EmailUnconfirmed:
htmlgen.a( htmlgen.a(
href=c.genSetUserStatusUrl(ui.nick, "ban"), href=c.genSetUserStatusUrl(ui.nick, "ban"),
"Ban user") "Ban user")
@ -955,22 +959,22 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string =
), ),
tr( tr(
th(""), th(""),
td(if c.userName == ui.nick or c.isAdmin: td(if c.rank >= Moderator and c.rank > ui.rank:
if ui.ban == "": if ui.rank == EmailUnconfirmed:
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "deactivate"),
"Deactivate user")
elif ui.ban == banReasonDeactivated:
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"),
"Activate user")
elif ui.ban == banReasonEmailUnconfirmed:
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"), htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"),
"Confirm user's email") "Confirm user's email")
elif ui.rank > Moderated:
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "deactivate"),
"Deactivate user")
elif ui.rank <= Moderated:
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"),
"Activate user")
else: "" else: ""
else: "") else: "")
), ),
tr( tr(
th(""), th(""),
td(if c.isAdmin: td(if c.rank >= Moderator:
htmlgen.a(href=c.req.makeUri("/deleteAll?nick=$1" % ui.nick), htmlgen.a(href=c.req.makeUri("/deleteAll?nick=$1" % ui.nick),
"Delete all user's posts and threads") "Delete all user's posts and threads")
else: "") else: "")
@ -1197,9 +1201,7 @@ routes:
"?") "?")
del = true del = true
of "deactivate": of "deactivate":
formBody.add "<input type='hidden' name='reason' value='" & formBody.add "<input type='submit' name='actBtn' value='Deactivate' />"
banReasonDeactivated & "' />" &
"<input type='submit' name='actBtn' value='Deactivate' />"
content = content =
htmlgen.p("Are you sure you wish to deactivate ", htmlgen.b(@"nick"), htmlgen.p("Are you sure you wish to deactivate ", htmlgen.b(@"nick"),
"?") "?")
@ -1218,10 +1220,11 @@ routes:
post "/dosetban": post "/dosetban":
createTFD() createTFD()
cond(@"nick" != "") cond(@"nick" != "")
if not c.isAdmin and @"nick" != c.userName: if c.rank < Moderator:
resp genMain(c, "You cannot ban this user.", "Error - Nim Forum") resp genMain(c, "You cannot ban this user.", "Error - Nim Forum")
if @"reason" == "" and @"del" != "true": if @"reason" == "" and @"del" != "true":
resp genMain(c, "Invalid ban reason.", "Error - Nim Forum") resp genMain(c, "Invalid ban reason.", "Error - Nim Forum")
let result = let result =
if @"del" == "true": if @"del" == "true":
# Remove the ban. # Remove the ban.
@ -1251,7 +1254,7 @@ routes:
post "/dodeleteall/?": post "/dodeleteall/?":
createTFD() createTFD()
cond(@"nick" != "") cond(@"nick" != "")
if not c.isAdmin: if c.rank < Moderator:
resp genMain(c, "You cannot delete this user's data.", "Error - Nim Forum") resp genMain(c, "You cannot delete this user's data.", "Error - Nim Forum")
let result = deleteAll(c, @"nick") let result = deleteAll(c, @"nick")
if result: if result:
@ -1264,7 +1267,7 @@ routes:
createTFD() createTFD()
cond(@"nick" != "") cond(@"nick" != "")
cond(@"pass" != "") cond(@"pass" != "")
if not c.isAdmin: if c.rank < Moderator:
resp genMain(c, "You cannot change this user's pass.", "Error - Nim Forum") resp genMain(c, "You cannot change this user's pass.", "Error - Nim Forum")
let res = setPassword(c, @"nick", @"pass") let res = setPassword(c, @"nick", @"pass")
if res: if res:
@ -1281,9 +1284,9 @@ routes:
cond(parseBiggestInt(@"epoch", epoch) > 0) cond(parseBiggestInt(@"epoch", epoch) > 0)
var success = false var success = false
if verifyIdentHash(c, @"nick", $epoch, @"ident"): if verifyIdentHash(c, @"nick", $epoch, @"ident"):
let ban = db.getValue(sql"select ban from person where name = ?", @"nick") let ban = parseEnum[Rank](db.getValue(sql"select status from person where name = ?", @"nick"))
if ban == banReasonEmailUnconfirmed: if ban == EmailUnconfirmed:
success = setBan(c, @"nick", "") success = setStatus(c, @"nick", Moderated)
if success: if success:
resp genMain(c, "Account activated", "Nim Forum") resp genMain(c, "Account activated", "Nim Forum")
@ -1361,11 +1364,11 @@ routes:
for i in 0 .. q.len-1: for i in 0 .. q.len-1:
if q[i].int < 32: q[i] = ' ' if q[i].int < 32: q[i] = ' '
elif q[i] == '\'': q[i] = '"' elif q[i] == '\'': q[i] = '"'
c.search = q.replace("\"","&quot;"); c.search = q.replace("\"","&quot;")
if @"page".len > 0: if @"page".len > 0:
parseInt(@"page", c.pageNum, 0..1000_000) parseInt(@"page", c.pageNum, 0..1000_000)
cond(c.pageNum > 0) cond(c.pageNum > 0)
iterator searchResults(): db_sqlite.TRow {.closure, tags: [FReadDB].} = iterator searchResults(): db_sqlite.Row {.closure, tags: [ReadDbEffect].} =
const queryFT = "fts.sql".slurp.sql const queryFT = "fts.sql".slurp.sql
for rowFT in fastRows(db, queryFT, for rowFT in fastRows(db, queryFT,
[q,q,$ThreadsPerPage,$c.pageNum,$ThreadsPerPage,q, [q,q,$ThreadsPerPage,$c.pageNum,$ThreadsPerPage,q,

12
ranks.nim Normal file
View file

@ -0,0 +1,12 @@
type
Rank* = enum ## serialized as 'status'
Spammer ## spammer: every post is invisible
Troll ## troll: cannot write new posts
Inactive ## member is not inactive
EmailUnconfirmed ## member with unconfirmed email address
Moderated ## new member: posts manually reviewed before everybody
## can see them
User ## Ordinary user
Moderator ## Moderator: can ban/troll/moderate users
Admin ## Admin: can do everything

View file

@ -1,7 +1,20 @@
import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs, import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs,
htmlparser, streams htmlparser, streams, parseutils
from times import getTime, getGMTime, format from times import getTime, getGMTime, format
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
noSideEffect.} =
## parses `s` into an integer in the range `validRange`. If successful,
## `value` is modified to contain the result. Otherwise no exception is
## raised and `value` is not touched; this way a reasonable default value
## won't be overwritten.
var x = value
try:
discard parseutils.parseInt(s, x, 0)
except OverflowError:
discard
if x in validRange: value = x
type type
Config* = object Config* = object
smtpAddress: string smtpAddress: string