Compare commits
No commits in common. "94c735b913c377a6fe7d861d58203ba44862bf01" and "497ec62990e84a29198755e4eb01c8780855c24e" have entirely different histories.
94c735b913
...
497ec62990
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,2 @@
|
|||||||
logs/
|
logs/
|
||||||
nginx.conf.compiled
|
nginx.conf.compiled
|
||||||
db.*.sqlite
|
|
||||||
.vscode/
|
|
||||||
.local/
|
|
||||||
static/
|
|
||||||
|
12
README.md
12
README.md
@ -1,12 +0,0 @@
|
|||||||
# Porom
|
|
||||||
porous forum
|
|
||||||
|
|
||||||
# deps
|
|
||||||
this is all off the top of my head so if you try to run it got help you
|
|
||||||
|
|
||||||
- lapis
|
|
||||||
- lsqlite3
|
|
||||||
- [magick](https://github.com/leafo/magick)
|
|
||||||
- bcrypt
|
|
||||||
|
|
||||||
i think thats it
|
|
18
app.lua
18
app.lua
@ -1,24 +1,6 @@
|
|||||||
local lapis = require("lapis")
|
local lapis = require("lapis")
|
||||||
local app = lapis.Application()
|
local app = lapis.Application()
|
||||||
|
|
||||||
local util = require("util")
|
|
||||||
|
|
||||||
app:enable("etlua")
|
|
||||||
app.layout = require "views.base"
|
|
||||||
|
|
||||||
local function inject_methods(req)
|
|
||||||
req.avatar_url = util.get_user_avatar_url
|
|
||||||
req.ntob = function(_, v)
|
|
||||||
return util.ntob(v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
app:before_filter(inject_methods)
|
|
||||||
|
|
||||||
app:include("apps.users", {path = "/user"})
|
|
||||||
app:include("apps.topics", {path = "/topics"})
|
|
||||||
app:include("apps.threads", {path = "/threads"})
|
|
||||||
|
|
||||||
app:get("/", function()
|
app:get("/", function()
|
||||||
return "Welcome to Lapis " .. require("lapis.version")
|
return "Welcome to Lapis " .. require("lapis.version")
|
||||||
end)
|
end)
|
||||||
|
119
apps/threads.lua
119
apps/threads.lua
@ -1,119 +0,0 @@
|
|||||||
local app = require("lapis").Application()
|
|
||||||
local lapis_util = require("lapis.util")
|
|
||||||
|
|
||||||
local db = require("lapis.db")
|
|
||||||
local util = require("util")
|
|
||||||
|
|
||||||
local models = require("models")
|
|
||||||
local Topics = models.Topics
|
|
||||||
local Threads = models.Threads
|
|
||||||
local Posts = models.Posts
|
|
||||||
|
|
||||||
app:get("thread_create", "/create", function(self)
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if not user then
|
|
||||||
self.session.flash = {error = "You must be logged in to perform this action."}
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end
|
|
||||||
local all_topics = db.query("select * from topics limit 25;")
|
|
||||||
if #all_topics == 0 then
|
|
||||||
return "how did you get here?"
|
|
||||||
end
|
|
||||||
self.all_topics = all_topics
|
|
||||||
return {render = "threads.create"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("thread_create", "/create", function(self)
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if not user then
|
|
||||||
self.session.flash = {error = "You must be logged in to perform this action."}
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end
|
|
||||||
local topic = Topics:find(self.params.topic_id)
|
|
||||||
if not topic then
|
|
||||||
return {redirect_to = self:url_for("topics")}
|
|
||||||
end
|
|
||||||
|
|
||||||
local title = lapis_util.trim(self.params.title)
|
|
||||||
local time = os.time()
|
|
||||||
local slug = lapis_util.slugify(title) .. "-" .. time
|
|
||||||
|
|
||||||
local post_content = self.params.initial_post
|
|
||||||
|
|
||||||
local thread = Threads:create({
|
|
||||||
topic_id = topic.id,
|
|
||||||
user_id = user.id,
|
|
||||||
title = title,
|
|
||||||
slug = slug,
|
|
||||||
created_at = time,
|
|
||||||
})
|
|
||||||
|
|
||||||
local post = util.create_post(thread.id, user.id, post_content)
|
|
||||||
if not post then
|
|
||||||
return {redirect_to = self:url_for("topics")}
|
|
||||||
end
|
|
||||||
|
|
||||||
return {redirect_to = self:url_for("thread", {slug = slug})}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:get("thread", "/:slug", function(self)
|
|
||||||
local thread = Threads:find({
|
|
||||||
slug = self.params.slug
|
|
||||||
})
|
|
||||||
if not thread then
|
|
||||||
return {status = 404}
|
|
||||||
end
|
|
||||||
self.thread = thread
|
|
||||||
local posts = db.query([[
|
|
||||||
SELECT
|
|
||||||
posts.id, post_history.content, users.username, avatars.file_path AS avatar_path
|
|
||||||
FROM
|
|
||||||
posts
|
|
||||||
JOIN
|
|
||||||
post_history ON posts.current_revision_id = post_history.id
|
|
||||||
JOIN
|
|
||||||
users ON posts.user_id = users.id
|
|
||||||
LEFT JOIN
|
|
||||||
avatars ON users.avatar_id = avatars.id
|
|
||||||
WHERE
|
|
||||||
posts.thread_id = ? and posts.id > ?
|
|
||||||
ORDER BY
|
|
||||||
posts.created_at ASC
|
|
||||||
LIMIT 20
|
|
||||||
]], thread.id, tonumber(self.params.cursor or 0))
|
|
||||||
self.user = util.get_logged_in_user_or_transient(self)
|
|
||||||
self.posts = posts
|
|
||||||
self.next_cursor = #posts > 0 and posts[#posts].id or nil
|
|
||||||
return {render = "threads.thread"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("thread", "/:slug", function(self)
|
|
||||||
local thread = Threads:find({
|
|
||||||
slug = self.params.slug
|
|
||||||
})
|
|
||||||
if not thread then
|
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
|
||||||
end
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if not user then
|
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
|
||||||
end
|
|
||||||
|
|
||||||
if user:is_guest() then
|
|
||||||
return {redirect_to = self:url_for("thread", {slug = thread.slug})}
|
|
||||||
end
|
|
||||||
|
|
||||||
if util.is_thread_locked(thread) and not user:is_admin() then
|
|
||||||
return {redirect_to = self:url_for("thread", {slug = thread.slug})}
|
|
||||||
end
|
|
||||||
|
|
||||||
local post_content = self.params.post_content
|
|
||||||
local post = util.create_post(thread.id, user.id, post_content)
|
|
||||||
if not post then
|
|
||||||
return {redirect_to = self:url_for("thread", {slug = thread.slug})}
|
|
||||||
end
|
|
||||||
|
|
||||||
return {redirect_to = self:url_for("thread", {slug = thread.slug})}
|
|
||||||
end)
|
|
||||||
|
|
||||||
return app
|
|
125
apps/topics.lua
125
apps/topics.lua
@ -1,125 +0,0 @@
|
|||||||
local app = require("lapis").Application()
|
|
||||||
local lapis_util = require("lapis.util")
|
|
||||||
|
|
||||||
local db = require("lapis.db")
|
|
||||||
local constants = require("constants")
|
|
||||||
|
|
||||||
local util = require("util")
|
|
||||||
|
|
||||||
local models = require("models")
|
|
||||||
local Users = models.Users
|
|
||||||
local Avatars = models.Avatars
|
|
||||||
local Topics = models.Topics
|
|
||||||
local Threads = models.Threads
|
|
||||||
|
|
||||||
local ThreadCreateError = {
|
|
||||||
OK = 0,
|
|
||||||
GUEST = 1,
|
|
||||||
LOGGED_OUT = 2,
|
|
||||||
TOPIC_LOCKED = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
app:get("all_topics", "", function(self)
|
|
||||||
self.topic_list = db.query("select * from topics limit 25;")
|
|
||||||
self.user = util.get_logged_in_user(self) or util.TransientUser
|
|
||||||
return {render = "topics.topics"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:get("topic_create", "/create", function(self)
|
|
||||||
local user = util.get_logged_in_user(self) or util.TransientUser
|
|
||||||
if not user:is_admin() then
|
|
||||||
return {status = 403}
|
|
||||||
end
|
|
||||||
|
|
||||||
return {render = "topics.create"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("topic_create", "/create", function(self)
|
|
||||||
local user = util.get_logged_in_user(self) or util.TransientUser
|
|
||||||
if not user:is_admin() then
|
|
||||||
return {redirect_to = "all_topics"}
|
|
||||||
end
|
|
||||||
|
|
||||||
local topic_name = lapis_util.trim(self.params.name)
|
|
||||||
local topic_description = self.params.description
|
|
||||||
local time = os.time()
|
|
||||||
local slug = lapis_util.slugify(topic_name) .. "-" .. time
|
|
||||||
|
|
||||||
local topic = Topics:create({
|
|
||||||
name = topic_name,
|
|
||||||
description = topic_description,
|
|
||||||
slug = slug,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:get("topic", "/:slug", function(self)
|
|
||||||
local topic = Topics:find({
|
|
||||||
slug = self.params.slug
|
|
||||||
})
|
|
||||||
if not topic then
|
|
||||||
return {status = 404}
|
|
||||||
end
|
|
||||||
self.topic = topic
|
|
||||||
self.threads_list = Threads:select(db.clause({
|
|
||||||
topic_id = topic.id
|
|
||||||
}))
|
|
||||||
local user = util.get_logged_in_user_or_transient(self)
|
|
||||||
print(topic.is_locked, type(topic.is_locked))
|
|
||||||
self.user = user
|
|
||||||
self.ThreadCreateError = ThreadCreateError
|
|
||||||
self.thread_create_error = ThreadCreateError.OK
|
|
||||||
if user:is_logged_in_guest() then
|
|
||||||
self.thread_create_error = ThreadCreateError.GUEST
|
|
||||||
elseif user:is_guest() then
|
|
||||||
self.thread_create_error = ThreadCreateError.LOGGED_OUT
|
|
||||||
elseif util.ntob(topic.is_locked) and not user:is_admin() then
|
|
||||||
self.thread_create_error = ThreadCreateError.TOPIC_LOCKED
|
|
||||||
end
|
|
||||||
|
|
||||||
return {render = "topics.topic"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:get("topic_edit", "/:slug/edit", function(self)
|
|
||||||
local user = util.get_logged_in_user_or_transient(self)
|
|
||||||
if not user:is_admin() then
|
|
||||||
return {redirect_to = self:url_for("topic", {slug = self.params.slug})}
|
|
||||||
end
|
|
||||||
local topic = Topics:find({
|
|
||||||
slug = self.params.slug
|
|
||||||
})
|
|
||||||
if not topic then
|
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
|
||||||
end
|
|
||||||
self.topic = topic
|
|
||||||
return {render = "topics.edit"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("topic_edit", "/:slug/edit", function(self)
|
|
||||||
local user = util.get_logged_in_user_or_transient(self)
|
|
||||||
if not user:is_admin() then
|
|
||||||
return {redirect_to = self:url_for("topic", {slug = self.params.slug})}
|
|
||||||
end
|
|
||||||
local topic = Topics:find({
|
|
||||||
slug = self.params.slug
|
|
||||||
})
|
|
||||||
if not topic then
|
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
|
||||||
end
|
|
||||||
local name = self.params.name or topic.name
|
|
||||||
local description = self.params.description or topic.description
|
|
||||||
local is_locked = topic.is_locked
|
|
||||||
if self.params.is_locked ~= nil then
|
|
||||||
is_locked = util.form_bool_to_sqlite(self.params.is_locked)
|
|
||||||
end
|
|
||||||
|
|
||||||
topic:update({
|
|
||||||
name = name,
|
|
||||||
description = description,
|
|
||||||
is_locked = is_locked,
|
|
||||||
})
|
|
||||||
return {redirect_to = self:url_for("topic", {slug = self.params.slug})}
|
|
||||||
end)
|
|
||||||
|
|
||||||
return app
|
|
318
apps/users.lua
318
apps/users.lua
@ -1,318 +0,0 @@
|
|||||||
local app = require("lapis").Application()
|
|
||||||
|
|
||||||
local db = require("lapis.db")
|
|
||||||
local constants = require("constants")
|
|
||||||
|
|
||||||
local util = require("util")
|
|
||||||
|
|
||||||
local bcrypt = require("bcrypt")
|
|
||||||
local rand = require("openssl.rand")
|
|
||||||
|
|
||||||
local models = require("models")
|
|
||||||
local Users = models.Users
|
|
||||||
local Sessions = models.Sessions
|
|
||||||
local Avatars = models.Avatars
|
|
||||||
|
|
||||||
local function authenticate_user(user, password)
|
|
||||||
return bcrypt.verify(password, user.password_hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function create_session_key()
|
|
||||||
return rand.bytes(16):gsub(".", function(c) return string.format("%02x", string.byte(c)) end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function create_session(user_id)
|
|
||||||
local days = 30
|
|
||||||
local expires_at = os.time() + (days * 24 * 60 * 60)
|
|
||||||
|
|
||||||
return Sessions:create({
|
|
||||||
key = create_session_key(),
|
|
||||||
user_id = user_id,
|
|
||||||
expires_at = expires_at,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
local function validate_password(password)
|
|
||||||
if #password < 10 or password:match("%s") then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
if #password > 255 then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local r = password:match("%u+") and
|
|
||||||
password:match("%l+") and
|
|
||||||
password:match("%d+") and
|
|
||||||
password:match("%p+")
|
|
||||||
return r ~= nil and true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function validate_username(username)
|
|
||||||
if #username < 3 or #username > 20 then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
return username:match("^[%w_-]+$") and true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function validate_url(url)
|
|
||||||
return url:match('^https?://.+$') and true
|
|
||||||
end
|
|
||||||
|
|
||||||
app:get("user", "/:username", function(self)
|
|
||||||
local user = Users:find({username = self.params.username})
|
|
||||||
if not user then
|
|
||||||
return {status = 404}
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.session.flash ~= nil and self.session.flash.just_logged_in then
|
|
||||||
self.just_logged_in = true
|
|
||||||
self.session.flash = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- local me = validate_session(self.session.session_key) or TransientUser
|
|
||||||
local me = util.get_logged_in_user(self) or util.TransientUser
|
|
||||||
self.user = user
|
|
||||||
self.me = me
|
|
||||||
|
|
||||||
self.user_is_me = me.id == user.id
|
|
||||||
|
|
||||||
if user.permission == constants.PermissionLevel.GUEST then
|
|
||||||
if not (self.user_is_me or me:is_admin()) then
|
|
||||||
return {status = 404}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return {render = "user.user"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("user_clear_avatar", "/:username/clear_avatar", function(self)
|
|
||||||
local me = util.get_logged_in_user(self)
|
|
||||||
if me == nil then
|
|
||||||
self.session.flash = {error = "You must be logged in to perform this action."}
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end
|
|
||||||
local target_user = Users:find({username = self.params.username})
|
|
||||||
if me.id ~= target_user.id then
|
|
||||||
return {redirect_to = self:url_for("user", {username = self.params.username})}
|
|
||||||
end
|
|
||||||
target_user:update({
|
|
||||||
avatar_id = db.NULL,
|
|
||||||
})
|
|
||||||
self.session.flash = {success = true, msg = "Avatar cleared."}
|
|
||||||
return {redirect_to = self:url_for("user_settings", {username = self.params.username})}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("user_set_avatar", "/:username/set_avatar", function(self)
|
|
||||||
local me = util.get_logged_in_user(self)
|
|
||||||
if me == nil then
|
|
||||||
self.session.flash = {error = "You must be logged in to perform this action."}
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end
|
|
||||||
local target_user = Users:find({username = self.params.username})
|
|
||||||
if me.id ~= target_user.id then
|
|
||||||
return {redirect_to = self:url_for("user", {username = self.params.username})}
|
|
||||||
end
|
|
||||||
local file = self.params.avatar
|
|
||||||
if not file then
|
|
||||||
self.session.flash = {error = "Something went wrong. Try again later."}
|
|
||||||
return {redirect_to = self:url_for("user_settings", {username = self.params.username})}
|
|
||||||
end
|
|
||||||
local time = os.time()
|
|
||||||
local filename = "u" .. target_user.id .. "d" .. time .. ".webp"
|
|
||||||
local proxied_filename = "/avatars/" .. filename
|
|
||||||
local save_path = "static" .. proxied_filename
|
|
||||||
local res = util.validate_and_create_image(file.content, save_path)
|
|
||||||
if not res then
|
|
||||||
self.session.flash = {error = "Something went wrong. Try again later."}
|
|
||||||
return {redirect_to = self:url_for("user_settings", {username = self.params.username})}
|
|
||||||
end
|
|
||||||
|
|
||||||
self.session.flash = {success = true, msg = "Avatar updated."}
|
|
||||||
local avatar = Avatars:create({
|
|
||||||
file_path = proxied_filename,
|
|
||||||
uploaded_at = time,
|
|
||||||
})
|
|
||||||
|
|
||||||
target_user:update({
|
|
||||||
avatar_id = avatar.id
|
|
||||||
})
|
|
||||||
|
|
||||||
return {redirect_to = self:url_for("user_settings", {username = self.params.username})}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:get("user_settings", "/:username/settings", function(self)
|
|
||||||
local me = util.get_logged_in_user(self)
|
|
||||||
if me == nil then
|
|
||||||
self.session.flash = {error = "You must be logged in to perform this action."}
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end
|
|
||||||
local target_user = Users:find({username = self.params.username})
|
|
||||||
if me.id ~= target_user.id then
|
|
||||||
return {redirect_to = self:url_for("user", {username = self.params.username})}
|
|
||||||
end
|
|
||||||
if self.session.flash then
|
|
||||||
local flash = self.session.flash
|
|
||||||
self.session.flash = nil
|
|
||||||
if flash.success then
|
|
||||||
self.flash_msg = flash.msg
|
|
||||||
elseif flash.error then
|
|
||||||
self.flash_msg = flash.error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.user = target_user
|
|
||||||
return {render = "user.settings"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("user_settings", "/:username/settings", function(self)
|
|
||||||
local me = util.get_logged_in_user(self)
|
|
||||||
if me == nil then
|
|
||||||
self.session.flash = {error = "You must be logged in to perform this action."}
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end
|
|
||||||
local target_user = Users:find({username = self.params.username})
|
|
||||||
if me.id ~= target_user.id then
|
|
||||||
return {redirect_to = self:url_for("user", {username = self.params.username})}
|
|
||||||
end
|
|
||||||
|
|
||||||
local status = self.params.status:sub(1, 100)
|
|
||||||
|
|
||||||
target_user:update({
|
|
||||||
status = status,
|
|
||||||
})
|
|
||||||
self.session.flash = {
|
|
||||||
success = true,
|
|
||||||
msg = "Settings updated."
|
|
||||||
}
|
|
||||||
return {redirect_to = self:url_for("user_settings", {username = self.params.username})}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:get("user_login", "/login", function(self)
|
|
||||||
if self.session.session_key then
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if user ~= nil then
|
|
||||||
return {redirect_to = self:url_for("user", {username = user.username})}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.session.flash then
|
|
||||||
self.err = self.session.flash.error
|
|
||||||
self.session.flash = {}
|
|
||||||
end
|
|
||||||
return {render = "user.login"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("user_login", "/login", function(self)
|
|
||||||
if self.session.session_key then
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if user ~= nil then
|
|
||||||
return {redirect_to = self:url_for("user", {username = user.username})}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local username = self.params.username
|
|
||||||
local password = self.params.password
|
|
||||||
local user = Users:find({username = username})
|
|
||||||
if not user then
|
|
||||||
self.session.flash = {error = "Invalid username or password"}
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end
|
|
||||||
if not authenticate_user(user, password) then
|
|
||||||
self.session.flash = {error = "Invalid username or password"}
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end
|
|
||||||
local session = create_session(user.id)
|
|
||||||
self.session.flash = {just_logged_in = true}
|
|
||||||
self.session.session_key = session.key
|
|
||||||
return {redirect_to = self:url_for("user", {username = username})}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:get("user_signup", "/signup", function(self)
|
|
||||||
if self.session.session_key then
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if user ~= nil then
|
|
||||||
return {redirect_to = self:url_for("user", {username = user.username})}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if self.session.flash then
|
|
||||||
self.err = self.session.flash.error
|
|
||||||
self.session.flash = {}
|
|
||||||
end
|
|
||||||
return {render = "user.signup"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("user_signup", "/signup", function(self)
|
|
||||||
if self.session.session_key then
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if user ~= nil then
|
|
||||||
return {redirect_to = self:url_for("user", {username = user.username})}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local username = self.params.username
|
|
||||||
local password = self.params.password
|
|
||||||
local password2 = self.params.password2
|
|
||||||
local user = Users:find({username = username})
|
|
||||||
if user then
|
|
||||||
self.session.flash = {error = "Username '" .. username .. "' is already taken."}
|
|
||||||
return {redirect_to = self:url_for("user_signup")}
|
|
||||||
end
|
|
||||||
|
|
||||||
if not validate_username(username) then
|
|
||||||
self.session.flash = {error = "Username must be 3-20 characters with only upper and lowercase letters, hyphens, and underscores."}
|
|
||||||
return {redirect_to = self:url_for("user_signup")}
|
|
||||||
end
|
|
||||||
|
|
||||||
if not validate_password(password) then
|
|
||||||
self.session.flash = {error = "Password must be 10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces."}
|
|
||||||
return {redirect_to = self:url_for("user_signup")}
|
|
||||||
end
|
|
||||||
|
|
||||||
if password ~= password2 then
|
|
||||||
self.session.flash = {error = "Passwords do not match."}
|
|
||||||
return {redirect_to = self:url_for("user_signup")}
|
|
||||||
end
|
|
||||||
|
|
||||||
local new_user = Users:create({
|
|
||||||
username = username,
|
|
||||||
password_hash = bcrypt.digest(password, constants.BCRYPT_ROUNDS),
|
|
||||||
permission = constants.PermissionLevel.GUEST,
|
|
||||||
})
|
|
||||||
|
|
||||||
local session = create_session(new_user.id)
|
|
||||||
self.session.flash = {just_logged_in = true}
|
|
||||||
self.session.session_key = session.key
|
|
||||||
return {redirect_to = self:url_for("user", {username = username})}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("user_logout", "/logout", function (self)
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if not user then
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end
|
|
||||||
|
|
||||||
local session = Sessions:find({key = self.session.session_key})
|
|
||||||
session:delete()
|
|
||||||
return {redirect_to = self:url_for("user_login")}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("confirm_user", "/confirm_user/:user_id", function (self)
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if not user then
|
|
||||||
return {status = 403}
|
|
||||||
end
|
|
||||||
if not user:is_admin() then
|
|
||||||
return {status = 403}
|
|
||||||
end
|
|
||||||
local target_user = Users:find(self.params.user_id)
|
|
||||||
if not target_user then
|
|
||||||
return {status = 404}
|
|
||||||
end
|
|
||||||
if target_user.permission > constants.PermissionLevel.GUEST then
|
|
||||||
return {status = 404}
|
|
||||||
end
|
|
||||||
|
|
||||||
target_user:update({permission = constants.PermissionLevel.USER, confirmed_on = os.time()})
|
|
||||||
return {redirect_to = self:url_for("user", {username = target_user.username})}
|
|
||||||
end)
|
|
||||||
|
|
||||||
return app
|
|
@ -3,10 +3,5 @@ local config = require("lapis.config")
|
|||||||
config("development", {
|
config("development", {
|
||||||
server = "nginx",
|
server = "nginx",
|
||||||
code_cache = "off",
|
code_cache = "off",
|
||||||
num_workers = "1",
|
num_workers = "1"
|
||||||
sqlite = {
|
|
||||||
database = "db.dev.sqlite"
|
|
||||||
},
|
|
||||||
secret = "SUPER SECRET",
|
|
||||||
session_name = "porom_session",
|
|
||||||
})
|
})
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
local Constants = {}
|
|
||||||
|
|
||||||
Constants.PermissionLevel = {
|
|
||||||
GUEST = 0,
|
|
||||||
USER = 1,
|
|
||||||
ADMIN = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
Constants.BCRYPT_ROUNDS = 10
|
|
||||||
|
|
||||||
return Constants
|
|
@ -1,32 +0,0 @@
|
|||||||
local bcrypt = require("bcrypt")
|
|
||||||
local models = require("models")
|
|
||||||
local constants = require("constants")
|
|
||||||
|
|
||||||
local alphabet = "-_@0123456789abcdefghijklmnopqrstuvwABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
|
|
||||||
local function create_admin()
|
|
||||||
local username = "admin"
|
|
||||||
local root_count = models.Users:count("username = ?", username)
|
|
||||||
if root_count ~= 0 then
|
|
||||||
print("admin account already exists.")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local password = ""
|
|
||||||
for _ = 1, 16 do
|
|
||||||
local randi = math.random(#alphabet)
|
|
||||||
password = password .. alphabet:sub(randi, randi)
|
|
||||||
end
|
|
||||||
|
|
||||||
local hash = bcrypt.digest(password, constants.BCRYPT_ROUNDS)
|
|
||||||
|
|
||||||
models.Users:create({
|
|
||||||
username = username,
|
|
||||||
password_hash = hash,
|
|
||||||
permission = constants.PermissionLevel.ADMIN,
|
|
||||||
})
|
|
||||||
|
|
||||||
print("Admin account created, use \"admin\" as the login and \"" .. password .. "\" as the password. This will only be shown once.")
|
|
||||||
end
|
|
||||||
|
|
||||||
create_admin()
|
|
@ -1,62 +0,0 @@
|
|||||||
local babycode = {}
|
|
||||||
|
|
||||||
local _escape_html = function(text)
|
|
||||||
return text:gsub("[&<>\"']", {
|
|
||||||
["&"] = "&",
|
|
||||||
["<"] = "<",
|
|
||||||
[">"] = ">",
|
|
||||||
['"'] = """,
|
|
||||||
["'"] = "'"
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
---renders babycode to html
|
|
||||||
---@param s string input babycode
|
|
||||||
---@param escape_html fun(s: string): string function that escapes html
|
|
||||||
function babycode.to_html(s, escape_html)
|
|
||||||
if not s or s == "" then return "" end
|
|
||||||
-- extract code blocks first and store them as placeholders
|
|
||||||
-- don't want to process bbcode embedded into a code block
|
|
||||||
local code_blocks = {}
|
|
||||||
local code_count = 0
|
|
||||||
local text = s:gsub("%[code%](.-)%[/code%]", function(code)
|
|
||||||
code_count = code_count + 1
|
|
||||||
code_blocks[code_count] = code
|
|
||||||
return "\1CODE:"..code_count.."\1"
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- replace `[url=https://example.com]Example[/url] tags
|
|
||||||
text = text:gsub("%[url=([^%]]+)%](.-)%[/url%]", function(url, label)
|
|
||||||
return '<a href="'..escape_html(url)..'">'..escape_html(label)..'</a>'
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- replace `[url]https://example.com[/url] tags
|
|
||||||
text = text:gsub("%[url%]([^%]]+)%[/url%]", function(url)
|
|
||||||
return '<a href="'..escape_html(url)..'">'..escape_html(url)..'</a>'
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- bold, italics, strikethrough
|
|
||||||
text = text:gsub("%[b%](.-)%[/b%]", "<strong>%1</strong>")
|
|
||||||
text = text:gsub("%[i%](.-)%[/i%]", "<em>%1</em>")
|
|
||||||
text = text:gsub("%[s%](.-)%[/s%]", "<del>%1</del>")
|
|
||||||
|
|
||||||
-- replace loose links
|
|
||||||
text = text:gsub("(https?://[%w-_%.%?%.:/%+=&~%@#%%]+[%w-/])", function(url)
|
|
||||||
if not text:find('<a[^>]*>'..url..'</a>') then
|
|
||||||
return '<a href="'..escape_html(url)..'">'..escape_html(url)..'</a>'
|
|
||||||
end
|
|
||||||
return url
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- replace code block placeholders back with their original contents
|
|
||||||
text = text:gsub("\1CODE:(%d+)\1", function(n)
|
|
||||||
return "<pre><code>"..code_blocks[tonumber(n)].."</code></pre>"
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- finally, normalize newlines replace them with <br>
|
|
||||||
text = text:gsub("\r?\n\r?\n+", "<br>"):gsub("\r?\n", "<br>")
|
|
||||||
|
|
||||||
return text
|
|
||||||
end
|
|
||||||
|
|
||||||
return babycode
|
|
@ -1,51 +0,0 @@
|
|||||||
local db = require("lapis.db")
|
|
||||||
local schema = require("lapis.db.schema")
|
|
||||||
local types = schema.types
|
|
||||||
|
|
||||||
return {
|
|
||||||
[1] = function ()
|
|
||||||
schema.create_table("sessions", {
|
|
||||||
{"id", types.integer{primary_key = true}},
|
|
||||||
{"key", types.text{unique = true}},
|
|
||||||
{"user_id", "INTEGER REFERENCES users(id) ON DELETE CASCADE"},
|
|
||||||
{"expires_at", types.integer},
|
|
||||||
{"created_at", "INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP))"},
|
|
||||||
})
|
|
||||||
|
|
||||||
db.query("CREATE INDEX sessions_user_id ON sessions(user_id)")
|
|
||||||
db.query("CREATE INDEX session_keys ON sessions(key)")
|
|
||||||
end,
|
|
||||||
|
|
||||||
[2] = function ()
|
|
||||||
schema.add_column("users", "confirmed_on", types.integer{null = true})
|
|
||||||
end,
|
|
||||||
|
|
||||||
[3] = function ()
|
|
||||||
schema.add_column("users", "status", types.text{null = true, default=""})
|
|
||||||
schema.create_table("avatars", {
|
|
||||||
{"id", types.integer{primary_key = true}},
|
|
||||||
{"file_path", types.text{unique = true}},
|
|
||||||
{"uploaded_at", "INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP))"},
|
|
||||||
})
|
|
||||||
schema.add_column("users", "avatar_id", "REFERENCES avatars(id) ON DELETE SET NULL")
|
|
||||||
end,
|
|
||||||
|
|
||||||
[4] = function ()
|
|
||||||
schema.add_column("topics", "description", types.text{default=""})
|
|
||||||
|
|
||||||
-- topic locked = no new threads can be created in the topic, but posts can be made in threads
|
|
||||||
-- thread locked = no new posts can be created in the thread, existing posts can not be edited
|
|
||||||
-- admins bypass both restrictions
|
|
||||||
schema.add_column("topics", "is_locked", "BOOLEAN DEFAULT FALSE")
|
|
||||||
schema.add_column("threads", "is_locked", "BOOLEAN DEFAULT FALSE")
|
|
||||||
-- will appear on top of non-stickied threads in topic view
|
|
||||||
schema.add_column("threads", "is_stickied", "BOOLEAN DEFAULT FALSE")
|
|
||||||
end,
|
|
||||||
|
|
||||||
[5] = function ()
|
|
||||||
db.query("CREATE INDEX idx_posts_thread ON posts(thread_id, created_at, id)")
|
|
||||||
db.query("CREATE INDEX idx_users_avatar ON users(avatar_id)")
|
|
||||||
db.query("CREATE INDEX idx_topics_slug ON topics(slug)")
|
|
||||||
db.query("CREATE INDEX idx_threads_slug ON threads(slug)")
|
|
||||||
end,
|
|
||||||
}
|
|
36
models.lua
36
models.lua
@ -1,34 +1,2 @@
|
|||||||
local Model = require("lapis.db.model").Model
|
local autoload = require("lapis.util").autoload
|
||||||
|
return autoload("models")
|
||||||
local constants = require("constants")
|
|
||||||
|
|
||||||
local Users, Users_mt = Model:extend("users")
|
|
||||||
|
|
||||||
function Users_mt:is_guest()
|
|
||||||
return self.permission == constants.PermissionLevel.GUEST
|
|
||||||
end
|
|
||||||
|
|
||||||
function Users_mt:is_admin()
|
|
||||||
return self.permission == constants.PermissionLevel.ADMIN
|
|
||||||
end
|
|
||||||
|
|
||||||
function Users_mt:is_logged_in_guest()
|
|
||||||
return self:is_guest() and true
|
|
||||||
end
|
|
||||||
|
|
||||||
function Users_mt:is_default_avatar()
|
|
||||||
return self.avatar_id == nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local ret = {
|
|
||||||
Users = Users,
|
|
||||||
Topics = Model:extend("topics"),
|
|
||||||
Threads = Model:extend("threads"),
|
|
||||||
Posts = Model:extend("posts"),
|
|
||||||
PostHistory = Model:extend("post_history"),
|
|
||||||
Sessions = Model:extend("sessions"),
|
|
||||||
Avatars = Model:extend("avatars"),
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
@ -32,10 +32,5 @@ http {
|
|||||||
location /favicon.ico {
|
location /favicon.ico {
|
||||||
alias static/favicon.ico;
|
alias static/favicon.ico;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /avatars {
|
|
||||||
alias static/avatars;
|
|
||||||
expires 1y;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
48
schema.lua
48
schema.lua
@ -1,48 +0,0 @@
|
|||||||
local schema = require("lapis.db.schema")
|
|
||||||
local db = require("lapis.db")
|
|
||||||
|
|
||||||
local types = schema.types
|
|
||||||
|
|
||||||
schema.create_table("users", {
|
|
||||||
{"id", types.integer{primary_key = true}},
|
|
||||||
{"username", types.text{unique = true, null = false}},
|
|
||||||
{"password_hash", types.text{null = false}},
|
|
||||||
{"permission", types.integer{default = 0}},
|
|
||||||
{"created_at", "INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP))"}
|
|
||||||
})
|
|
||||||
|
|
||||||
schema.create_table("topics", {
|
|
||||||
{"id", types.integer{primary_key = true}},
|
|
||||||
{"name", types.text{null = false}},
|
|
||||||
{"slug", types.text{null = false, unique = true}}
|
|
||||||
})
|
|
||||||
|
|
||||||
schema.create_table("threads", {
|
|
||||||
{"id", types.integer{primary_key = true}},
|
|
||||||
{"topic_id", "INTEGER REFERENCES topics(id) ON DELETE CASCADE"},
|
|
||||||
{"user_id", "INTEGER REFERENCES users(id) ON DELETE SET NULL"},
|
|
||||||
{"title", types.text{null = false}},
|
|
||||||
{"slug", types.text{null = false, unique = true}},
|
|
||||||
{"created_at", "INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP))"},
|
|
||||||
})
|
|
||||||
|
|
||||||
schema.create_table("posts", {
|
|
||||||
{"id", types.integer{primary_key = true}},
|
|
||||||
{"thread_id", "INTEGER REFERENCES threads(id) ON DELETE CASCADE"},
|
|
||||||
{"user_id", "INTEGER REFERENCES users(id) ON DELETE SET NULL"},
|
|
||||||
{"created_at", "INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP))"},
|
|
||||||
{"current_revision_id", "INTEGER REFERENCES post_history(id)"},
|
|
||||||
})
|
|
||||||
|
|
||||||
schema.create_table("post_history", {
|
|
||||||
{"id", types.integer{primary_key = true}},
|
|
||||||
{"post_id", "INTEGER REFERENCES posts(id) ON DELETE CASCADE"},
|
|
||||||
{"user_id", "INTEGER REFERENCES users(id) ON DELETE CASCADE"},
|
|
||||||
{"content", types.text{null = false}},
|
|
||||||
{"edited_at", "INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP))"},
|
|
||||||
{"is_initial_revision", "BOOLEAN DEFAULT FALSE"}
|
|
||||||
})
|
|
||||||
|
|
||||||
db.query("CREATE INDEX idx_threads_topic_id ON threads(topic_id)")
|
|
||||||
db.query("CREATE INDEX idx_posts_thread_id ON posts(thread_id)")
|
|
||||||
db.query("CREATE INDEX idx_post_history_post_id ON post_history(post_id)")
|
|
130
util.lua
130
util.lua
@ -1,130 +0,0 @@
|
|||||||
local util = {}
|
|
||||||
local magick = require("magick")
|
|
||||||
local db = require("lapis.db")
|
|
||||||
local html_escape = require("lapis.html").escape
|
|
||||||
|
|
||||||
local Avatars = require("models").Avatars
|
|
||||||
local Users = require("models").Users
|
|
||||||
local Posts = require("models").Posts
|
|
||||||
local PostHistory = require("models").PostHistory
|
|
||||||
|
|
||||||
local babycode = require("lib.babycode")
|
|
||||||
|
|
||||||
util.TransientUser = {
|
|
||||||
is_admin = function (self)
|
|
||||||
return false
|
|
||||||
end,
|
|
||||||
is_guest = function (self)
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
is_logged_in_guest = function (self)
|
|
||||||
return false
|
|
||||||
end,
|
|
||||||
username = "Deleted User",
|
|
||||||
}
|
|
||||||
|
|
||||||
function util.get_user_avatar_url(req, user)
|
|
||||||
if not user.avatar_id then
|
|
||||||
return "/avatars/default.webp"
|
|
||||||
end
|
|
||||||
return Avatars:find(user.avatar_id).file_path
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.validate_and_create_image(input_image, filename)
|
|
||||||
local img = magick.load_image_from_blob(input_image)
|
|
||||||
|
|
||||||
if not img then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
img:strip()
|
|
||||||
img:set_gravity("CenterGravity")
|
|
||||||
|
|
||||||
local width, height = img:get_width(), img:get_height()
|
|
||||||
local min_dim = math.min(width, height)
|
|
||||||
if min_dim > 256 then
|
|
||||||
local ratio = 256.0 / min_dim
|
|
||||||
local new_w, new_h = width * ratio, height * ratio
|
|
||||||
img:resize(new_w, new_h)
|
|
||||||
end
|
|
||||||
|
|
||||||
width, height = img:get_width(), img:get_height()
|
|
||||||
local crop_size = math.min(width, height)
|
|
||||||
local x_offset = (width - crop_size) / 2
|
|
||||||
local y_offset = (height - crop_size) / 2
|
|
||||||
img:crop(crop_size, crop_size, x_offset, y_offset)
|
|
||||||
|
|
||||||
img:set_format("webp")
|
|
||||||
img:set_quality(85)
|
|
||||||
|
|
||||||
img:write(filename)
|
|
||||||
img:destroy()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.get_logged_in_user(req)
|
|
||||||
if req.session.session_key == nil then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local session = db.select('* FROM "sessions" WHERE "key" = ? AND "expires_at" > "?" LIMIT 1', req.session.session_key, os.time())
|
|
||||||
if #session > 0 then
|
|
||||||
return Users:find({id = session[1].user_id})
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.get_logged_in_user_or_transient(req)
|
|
||||||
return util.get_logged_in_user(req) or util.TransientUser
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.ntob(v)
|
|
||||||
return v ~= 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.bton(b)
|
|
||||||
return 1 and b or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.stob(s)
|
|
||||||
if s == "true" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
if s == "false" then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.form_bool_to_sqlite(s)
|
|
||||||
return util.bton(util.stob(s))
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.is_thread_locked(thread)
|
|
||||||
return util.ntob(thread.is_locked)
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.create_post(thread_id, user_id, content)
|
|
||||||
db.query("BEGIN")
|
|
||||||
local post = Posts:create({
|
|
||||||
thread_id = thread_id,
|
|
||||||
user_id = user_id,
|
|
||||||
current_revision_id = db.NULL,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bb_content = babycode.to_html(content, html_escape)
|
|
||||||
|
|
||||||
local revision = PostHistory:create({
|
|
||||||
post_id = post.id,
|
|
||||||
user_id = user_id,
|
|
||||||
content = bb_content,
|
|
||||||
is_initial_revision = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
post:update({current_revision_id = revision.id})
|
|
||||||
|
|
||||||
db.query("COMMIT")
|
|
||||||
return post
|
|
||||||
end
|
|
||||||
|
|
||||||
return util
|
|
@ -1,10 +0,0 @@
|
|||||||
<!DOCTYPE HTML>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Porom</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<% content_for("inner") %>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,13 +0,0 @@
|
|||||||
<h1>New thread</h1>
|
|
||||||
<form method="post">
|
|
||||||
<label for="topic_id">Topic:</label>
|
|
||||||
<select name="topic_id", id="topic_id" autocomplete="off">
|
|
||||||
<% for _, topic in ipairs(all_topics) do %>
|
|
||||||
<option value="<%= topic.id %>" <%- params.topic_id == tostring(topic.id) and "selected" or "" %>><%= topic.name %></value>
|
|
||||||
<% end %>
|
|
||||||
</select><br>
|
|
||||||
<label for="title">Thread title:</label>
|
|
||||||
<input type="text" id="title" name="title" required><br>
|
|
||||||
<textarea id="initial_post" name="initial_post" placeholder="Post body" required></textarea><br>
|
|
||||||
<input type="submit" value="Create thread">
|
|
||||||
</form>
|
|
@ -1,20 +0,0 @@
|
|||||||
<% for _, post in ipairs(posts) do %>
|
|
||||||
<div>
|
|
||||||
<img src="<%= post.avatar_path or "/avatars/default.webp" %>">
|
|
||||||
<div><%= post.username %></div>
|
|
||||||
<div><p><%- post.content %></p></div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if not user:is_guest() then %>
|
|
||||||
<h1>Respond to "<%= thread.title %>"</h1>
|
|
||||||
<form method="post">
|
|
||||||
<textarea id="post_content" name="post_content" placeholder="Response body" required></textarea><br>
|
|
||||||
<input type="submit" value="Reply">
|
|
||||||
</form>
|
|
||||||
<% end %>
|
|
||||||
<% if next_cursor then %>
|
|
||||||
<a href="<%= url_for('thread', {slug = thread.slug}, {cursor = next_cursor}) %>">
|
|
||||||
Older posts →
|
|
||||||
</a>
|
|
||||||
<% end %>
|
|
@ -1,6 +0,0 @@
|
|||||||
<h1>Create topic</h1>
|
|
||||||
<form method="post">
|
|
||||||
<input type="text" name="name" id="name" placeholder="Topic name" required><br>
|
|
||||||
<textarea id="description" name="description" placeholder="Topic description" required></textarea><br>
|
|
||||||
<input type="submit" value="Create topic">
|
|
||||||
</form>
|
|
@ -1,12 +0,0 @@
|
|||||||
<h1>Editing topic <%= topic.name %></h1>
|
|
||||||
<form method="post">
|
|
||||||
<input type="text" name="name" id="name" value="<%= topic.name %>" placeholder="Topic name" required><br>
|
|
||||||
<textarea id="description" name="description" value="<%= topic.description %>" placeholder="Topic description"></textarea><br>
|
|
||||||
<input type="checkbox" id="is_locked" name="is_locked" value="<%= ntob(topic.is_locked) %>">
|
|
||||||
<label for="is_locked">Locked</label><br>
|
|
||||||
<input type="submit" value="Save changes">
|
|
||||||
</form>
|
|
||||||
<form method="get" action="<%= url_for("topic", {slug = topic.slug}) %>">
|
|
||||||
<input type="submit" value="Cancel">
|
|
||||||
</form>
|
|
||||||
<i>Note: to preserve history, you cannot change the topic URL.</i>
|
|
@ -1,25 +0,0 @@
|
|||||||
<h1><%= topic.name %></h1>
|
|
||||||
<h2><%= topic.description %></h2>
|
|
||||||
<% if #threads_list == 0 then %>
|
|
||||||
<p>There are no threads in this topic.</p>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if thread_create_error == ThreadCreateError.OK then %>
|
|
||||||
<a href=<%= url_for("thread_create", nil, {topic_id = topic.id}) %>>New thread</a>
|
|
||||||
<% elseif thread_create_error == ThreadCreateError.GUEST then %>
|
|
||||||
<p>Your account is still pending confirmation by an administrator. You are not able to create a new thread or post at this time.</p>
|
|
||||||
<% elseif thread_create_error == ThreadCreateError.LOGGED_OUT then %>
|
|
||||||
<p>Only logged in users can create threads. <a href="<%= url_for("user_signup") %>">Sign up</a> or <a href="<%= url_for("user_login")%>">log in</a> to create a thread.</p>
|
|
||||||
<% else %>
|
|
||||||
<p>This topic is locked.</p>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if user:is_admin() then %>
|
|
||||||
<br>
|
|
||||||
<a href="<%= url_for("topic_edit", {slug = topic.slug}) %>">Edit topic</a>
|
|
||||||
<form method="post" action="<%= url_for("topic_edit", {slug = topic.slug}) %>">
|
|
||||||
<input type="hidden" name="is_locked" value="<%= not ntob(topic.is_locked) %>">
|
|
||||||
<p><%= "This topic is " .. (ntob(topic.is_locked) and "" or "un") .. "locked." %></p>
|
|
||||||
<input type="submit" id="lock" value="<%= ntob(topic.is_locked) and "Unlock" or "Lock" %>">
|
|
||||||
</form>
|
|
||||||
<% end %>
|
|
@ -1,16 +0,0 @@
|
|||||||
<h1>Topics</h1>
|
|
||||||
|
|
||||||
<% if #topic_list == 0 then %>
|
|
||||||
<p>There are no topics.</p>
|
|
||||||
<% else %>
|
|
||||||
<ul>
|
|
||||||
<% for i, v in ipairs(topic_list) do %>
|
|
||||||
<li>
|
|
||||||
<a href=<%= url_for("topic", {slug = v.slug}) %>><%= v.name %></a> - <%= v.description %>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
<% if user:is_admin() then %>
|
|
||||||
<a href="<%= url_for("topic_create") %>">Create new topic</a>
|
|
||||||
<% end %>
|
|
@ -1,12 +0,0 @@
|
|||||||
<h1>Log In</h1>
|
|
||||||
|
|
||||||
<% if err then %>
|
|
||||||
<h2><%= err %></h2>
|
|
||||||
<% end %>
|
|
||||||
<form method="post" action="<%= url_for('user_login') %>" enctype="multipart/form-data">
|
|
||||||
<label for="username">Username</label><br>
|
|
||||||
<input type="text" id="username" name="username" required autocomplete="username"><br>
|
|
||||||
<label for="password">Password</label><br>
|
|
||||||
<input type="password" id="password" name="password" required autocomplete="current-password"><br>
|
|
||||||
<input type="submit" value="Log in">
|
|
||||||
</form>
|
|
@ -1,18 +0,0 @@
|
|||||||
<h1>User settings</h1>
|
|
||||||
<% if flash_msg then %>
|
|
||||||
<h2><%= flash_msg %></h2>
|
|
||||||
<% end %>
|
|
||||||
<form method="post" action="<%= url_for("user_set_avatar", {username = user.username}) %>" enctype="multipart/form-data">
|
|
||||||
<img src="<%= avatar_url(user) %>"><br>
|
|
||||||
<input type="file" name="avatar" accept="image/*"><br>
|
|
||||||
<input type="submit" value="Update avatar">
|
|
||||||
<% if not user:is_default_avatar() then %>
|
|
||||||
<input type="submit" value="Clear avatar" formaction="<%= url_for("user_clear_avatar", {username = user.username}) %>">
|
|
||||||
<% end %>
|
|
||||||
<br>
|
|
||||||
</form>
|
|
||||||
<form method="post" action="">
|
|
||||||
<label for="status">Status</label>
|
|
||||||
<input type="text" id="status" name="status" value="<%= user.status %>" maxlength="10"><br>
|
|
||||||
<input type="submit" value="Save">
|
|
||||||
</form>
|
|
@ -1,15 +0,0 @@
|
|||||||
<h1>Sign up</h1>
|
|
||||||
|
|
||||||
<% if err then %>
|
|
||||||
<h2><%= err %></h2>
|
|
||||||
<% end %>
|
|
||||||
<form method="post" action="<%= url_for('user_signup') %>" enctype="multipart/form-data">
|
|
||||||
<label for="username">Username</label><br>
|
|
||||||
<input type="text" id="username" name="username" pattern="[\w\-]{3,20}" title="3-20 characters. Only upper and lowercase letters, hyphens, and underscores" required autocomplete="username"><br>
|
|
||||||
<label for="password">Password</label><br>
|
|
||||||
<input type="password" id="password" name="password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
|
|
||||||
<label for="password2">Confirm Password</label><br>
|
|
||||||
<input type="password" id="password2" name="password2" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
|
|
||||||
<input type="submit" value="Sign up">
|
|
||||||
</form>
|
|
||||||
<p>After you sign up, an administrator will need to confirm your account before you will be allowed to post.</p>
|
|
@ -1,21 +0,0 @@
|
|||||||
<% if just_logged_in then %>
|
|
||||||
<h1>Logged in successfully.</h1>
|
|
||||||
<% end %>
|
|
||||||
<img src="<%= avatar_url(user) %>">
|
|
||||||
<h1><%= user.username %></h1>
|
|
||||||
<% if user:is_guest() and user_is_me then %>
|
|
||||||
<h2>You are a guest. An administrator needs to approve your account before you will be able to post.</h2>
|
|
||||||
<% end %>
|
|
||||||
<% if user_is_me then %>
|
|
||||||
<form method="post" action="<%= url_for("user_logout", {user_id = me.id}) %>">
|
|
||||||
<input type="submit" value="Log out">
|
|
||||||
</form>
|
|
||||||
<% end %>
|
|
||||||
<% if me:is_admin() and user:is_guest() then %>
|
|
||||||
<p>This user is a guest. They signed up on <%= os.date("%c", user.created_at) %>.</p>
|
|
||||||
<form method="post" action="<%= url_for("confirm_user", {user_id = user.id}) %>">
|
|
||||||
<input type="submit" value="Confirm user">
|
|
||||||
</form>
|
|
||||||
<% elseif me:is_admin() then %>
|
|
||||||
<p>This user signed up on <%= os.date("%c", user.created_at) %> and was confirmed on <%= os.date("%c", user.confirmed_on) %>.</p>
|
|
||||||
<% end %>
|
|
Loading…
Reference in New Issue
Block a user