426 lines
13 KiB
Lua
426 lines
13 KiB
Lua
local app = require("lapis").Application()
|
|
|
|
local db = require("lapis.db")
|
|
local constants = require("constants")
|
|
|
|
local util = require("util")
|
|
|
|
local auth = require("lib.auth")
|
|
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 auth.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
|
|
|
|
local me = util.get_logged_in_user_or_transient(self)
|
|
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_mod()) then
|
|
return {status = 404}
|
|
end
|
|
end
|
|
|
|
self.latest_posts = db.query([[
|
|
SELECT
|
|
posts.id, posts.created_at, post_history.content, post_history.edited_at, threads.title AS thread_title, topics.name as topic_name, threads.slug as thread_slug
|
|
FROM
|
|
posts
|
|
JOIN
|
|
post_history ON posts.current_revision_id = post_history.id
|
|
JOIN
|
|
threads ON posts.thread_id = threads.id
|
|
JOIN
|
|
topics ON threads.topic_id = topics.id
|
|
WHERE
|
|
posts.user_id = ?
|
|
ORDER BY posts.created_at DESC
|
|
LIMIT 10
|
|
]], user.id)
|
|
|
|
self.page_title = user.username .. "'s profile"
|
|
|
|
return {render = "user.user"}
|
|
end)
|
|
|
|
app:post("user_delete", "/:username/delete", function(self)
|
|
-- this route explicitly does not handle admins deleting other users
|
|
-- i might make a separate route for it later, but guesting users is possible
|
|
local me = util.get_logged_in_user(self)
|
|
if me == nil then
|
|
util.inject_err_infobox(self, "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 not authenticate_user(target_user, self.params.password) then
|
|
util.inject_err_infobox(self, "The password you entered is incorrect.")
|
|
return {redirect_to = self:url_for("user_delete_confirm", {username = me.username})}
|
|
end
|
|
|
|
util.transfer_and_delete_user(target_user)
|
|
util.inject_infobox(self, "Your account has been added to the deletion queue.")
|
|
return {redirect_to = self:url_for("user_signup")}
|
|
end)
|
|
|
|
app:get("user_delete_confirm", "/:username/delete_confirm", function(self)
|
|
local me = util.get_logged_in_user(self)
|
|
if me == nil then
|
|
-- util.inject_err_infobox(self, "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
|
|
self.me = target_user
|
|
self.page_title = "confirm deletion"
|
|
|
|
return {render = "user.delete_confirm"}
|
|
end)
|
|
|
|
app:post("user_clear_avatar", "/:username/clear_avatar", function(self)
|
|
local me = util.get_logged_in_user(self)
|
|
if me == nil then
|
|
util.inject_err_infobox(self, "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 old_avatar_id = target_user.avatar_id
|
|
target_user:update({
|
|
avatar_id = 1,
|
|
})
|
|
util.destroy_avatar(old_avatar_id)
|
|
util.inject_infobox(self, "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
|
|
util.inject_err_infobox(self, "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
|
|
util.inject_warn_infobox(self, "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
|
|
util.inject_warn_infobox(self, "Something went wrong. Try again later.")
|
|
return {redirect_to = self:url_for("user_settings", {username = self.params.username})}
|
|
end
|
|
|
|
util.inject_infobox(self, "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
|
|
util.inject_err_infobox(self, "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
|
|
self.me = target_user
|
|
self.page_title = "settings"
|
|
|
|
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
|
|
util.inject_err_infobox(self, "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,
|
|
})
|
|
util.inject_infobox(self, "Status 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
|
|
|
|
self.page_title = "log in"
|
|
|
|
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
|
|
util.inject_err_infobox(self, "Invalid username or password")
|
|
return {redirect_to = self:url_for("user_login")}
|
|
end
|
|
if user.permission == constants.PermissionLevel.SYSTEM then
|
|
util.inject_err_infobox(self, "Invalid username or password")
|
|
return {redirect_to = self:url_for("user_login")}
|
|
end
|
|
if not authenticate_user(user, password) then
|
|
util.inject_err_infobox(self, "Invalid username or password")
|
|
return {redirect_to = self:url_for("user_login")}
|
|
end
|
|
local session = create_session(user.id)
|
|
util.inject_infobox(self, "Logged in successfully.")
|
|
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
|
|
|
|
self.page_title = "sign up"
|
|
|
|
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
|
|
util.inject_err_infobox(self, "Username '" .. username .. "' is already taken.")
|
|
return {redirect_to = self:url_for("user_signup")}
|
|
end
|
|
|
|
if not validate_username(username) then
|
|
util.inject_err_infobox(self, "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
|
|
util.inject_err_infobox(self, "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
|
|
util.inject_err_infobox(self, "Passwords do not match.")
|
|
return {redirect_to = self:url_for("user_signup")}
|
|
end
|
|
|
|
local new_user = Users:create({
|
|
username = username,
|
|
password_hash = auth.digest(password),
|
|
permission = constants.PermissionLevel.GUEST,
|
|
})
|
|
|
|
local session = create_session(new_user.id)
|
|
util.inject_infobox(self, "Siged up successfully.")
|
|
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_mod() 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)
|
|
|
|
app:post("mod_user", "/mod_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:is_mod() then
|
|
return {status = 404}
|
|
end
|
|
|
|
target_user:update({permission = constants.PermissionLevel.MODERATOR})
|
|
return {redirect_to = self:url_for("user", {username = target_user.username})}
|
|
end)
|
|
|
|
app:post("demod_user", "/demod_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 not target_user:is_mod() then
|
|
return {status = 404}
|
|
end
|
|
|
|
target_user:update({permission = constants.PermissionLevel.USER})
|
|
return {redirect_to = self:url_for("user", {username = target_user.username})}
|
|
end)
|
|
|
|
app:post("guest_user", "/guest_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_mod() 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:is_mod() then
|
|
return {status = 404}
|
|
end
|
|
|
|
target_user:update({permission = constants.PermissionLevel.GUEST})
|
|
return {redirect_to = self:url_for("user", {username = target_user.username})}
|
|
end)
|
|
|
|
return app
|