Compare commits
10 Commits
497ec62990
...
94c735b913
Author | SHA1 | Date | |
---|---|---|---|
94c735b913 | |||
785eafd646 | |||
4039d6d299 | |||
f5485702a8 | |||
86b568d0f4 | |||
836ad72521 | |||
9c327957d9 | |||
ac51e5c0e8 | |||
03a20128f7 | |||
91d4fa59f3 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,2 +1,6 @@
|
||||
logs/
|
||||
nginx.conf.compiled
|
||||
db.*.sqlite
|
||||
.vscode/
|
||||
.local/
|
||||
static/
|
||||
|
12
README.md
Normal file
12
README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# 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,6 +1,24 @@
|
||||
local lapis = require("lapis")
|
||||
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()
|
||||
return "Welcome to Lapis " .. require("lapis.version")
|
||||
end)
|
||||
|
119
apps/threads.lua
Normal file
119
apps/threads.lua
Normal file
@ -0,0 +1,119 @@
|
||||
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
Normal file
125
apps/topics.lua
Normal file
@ -0,0 +1,125 @@
|
||||
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
Normal file
318
apps/users.lua
Normal file
@ -0,0 +1,318 @@
|
||||
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,5 +3,10 @@ local config = require("lapis.config")
|
||||
config("development", {
|
||||
server = "nginx",
|
||||
code_cache = "off",
|
||||
num_workers = "1"
|
||||
num_workers = "1",
|
||||
sqlite = {
|
||||
database = "db.dev.sqlite"
|
||||
},
|
||||
secret = "SUPER SECRET",
|
||||
session_name = "porom_session",
|
||||
})
|
||||
|
11
constants.lua
Normal file
11
constants.lua
Normal file
@ -0,0 +1,11 @@
|
||||
local Constants = {}
|
||||
|
||||
Constants.PermissionLevel = {
|
||||
GUEST = 0,
|
||||
USER = 1,
|
||||
ADMIN = 2,
|
||||
}
|
||||
|
||||
Constants.BCRYPT_ROUNDS = 10
|
||||
|
||||
return Constants
|
32
create_default_admin.lua
Normal file
32
create_default_admin.lua
Normal file
@ -0,0 +1,32 @@
|
||||
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()
|
62
lib/babycode.lua
Normal file
62
lib/babycode.lua
Normal file
@ -0,0 +1,62 @@
|
||||
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
|
51
migrations.lua
Normal file
51
migrations.lua
Normal file
@ -0,0 +1,51 @@
|
||||
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,2 +1,34 @@
|
||||
local autoload = require("lapis.util").autoload
|
||||
return autoload("models")
|
||||
local Model = require("lapis.db.model").Model
|
||||
|
||||
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,5 +32,10 @@ http {
|
||||
location /favicon.ico {
|
||||
alias static/favicon.ico;
|
||||
}
|
||||
|
||||
location /avatars {
|
||||
alias static/avatars;
|
||||
expires 1y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
48
schema.lua
Normal file
48
schema.lua
Normal file
@ -0,0 +1,48 @@
|
||||
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
Normal file
130
util.lua
Normal file
@ -0,0 +1,130 @@
|
||||
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
|
10
views/base.etlua
Normal file
10
views/base.etlua
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Porom</title>
|
||||
</head>
|
||||
<body>
|
||||
<% content_for("inner") %>
|
||||
</body>
|
||||
</html>
|
13
views/threads/create.etlua
Normal file
13
views/threads/create.etlua
Normal file
@ -0,0 +1,13 @@
|
||||
<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>
|
20
views/threads/thread.etlua
Normal file
20
views/threads/thread.etlua
Normal file
@ -0,0 +1,20 @@
|
||||
<% 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 %>
|
6
views/topics/create.etlua
Normal file
6
views/topics/create.etlua
Normal file
@ -0,0 +1,6 @@
|
||||
<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>
|
12
views/topics/edit.etlua
Normal file
12
views/topics/edit.etlua
Normal file
@ -0,0 +1,12 @@
|
||||
<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>
|
25
views/topics/topic.etlua
Normal file
25
views/topics/topic.etlua
Normal file
@ -0,0 +1,25 @@
|
||||
<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 %>
|
16
views/topics/topics.etlua
Normal file
16
views/topics/topics.etlua
Normal file
@ -0,0 +1,16 @@
|
||||
<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 %>
|
12
views/user/login.etlua
Normal file
12
views/user/login.etlua
Normal file
@ -0,0 +1,12 @@
|
||||
<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>
|
18
views/user/settings.etlua
Normal file
18
views/user/settings.etlua
Normal file
@ -0,0 +1,18 @@
|
||||
<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>
|
15
views/user/signup.etlua
Normal file
15
views/user/signup.etlua
Normal file
@ -0,0 +1,15 @@
|
||||
<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>
|
21
views/user/user.etlua
Normal file
21
views/user/user.etlua
Normal file
@ -0,0 +1,21 @@
|
||||
<% 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