local app = require("lapis").Application() local babycode = require("lib.babycode") local html_escape = require("lapis.html").escape 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 = "data/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) local original_sig = self.params.signature or "" local rendered_sig = babycode.to_html(original_sig, html_escape) target_user:update({ status = status, signature_original_markup = original_sig, signature_rendered = rendered_sig, }) util.inject_infobox(self, "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 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