starting users

This commit is contained in:
Lera Elvoé 2025-05-18 05:41:26 +03:00
parent 03a20128f7
commit ac51e5c0e8
Signed by: yagich
SSH Key Fingerprint: SHA256:6xjGb6uA7lAVcULa7byPEN//rQ0wPoG+UzYVMfZnbvc
12 changed files with 314 additions and 2 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ logs/
nginx.conf.compiled
db.*.sqlite
.vscode/
.local/

View File

@ -1,6 +1,11 @@
local lapis = require("lapis")
local app = lapis.Application()
app:enable("etlua")
app.layout = require "views.base"
app:include("apps.users", {path = "/user"})
app:get("/", function()
return "Welcome to Lapis " .. require("lapis.version")
end)

181
apps/users.lua Normal file
View File

@ -0,0 +1,181 @@
local app = require("lapis").Application()
local db = require("lapis.db")
local constants = require("constants")
local bcrypt = require("bcrypt")
local rand = require("openssl.rand")
local models = require("models")
local Users = models.Users
local Sessions = models.Sessions
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_session(session_key)
local session = db.select('* FROM "sessions" WHERE "key" = ? AND "expires_at" > "?" LIMIT 1', session_key, os.time())
print(#session)
if #session > 0 then
return Users:find({id = session[1].user_id})
end
return nil
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
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.just_logged_in then
self.just_logged_in = true
self.session.flash = {}
end
local me = validate_session(self.session.session_key)
if not me and user.permission == constants.PermissionLevel.GUEST then
return {status = 404}
end
self.user = user
return {render = "user.user"}
end)
app:get("user_login", "/login", function(self)
if self.session.session_key then
local user = validate_session(self.session.session_key)
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 = validate_session(self.session.session_key)
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 = validate_session(self.session.session_key)
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 = validate_session(self.session.session_key)
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)
return app

View File

@ -8,4 +8,5 @@ config("development", {
database = "db.dev.sqlite"
},
secret = "SUPER SECRET",
session_name = "porom_session",
})

11
constants.lua Normal file
View 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
View 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()

18
migrations.lua Normal file
View File

@ -0,0 +1,18 @@
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
}

View File

@ -1,2 +1,21 @@
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
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"),
}
return ret

10
views/base.etlua Normal file
View 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>

12
views/user/login.etlua Normal file
View 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>

15
views/user/signup.etlua Normal file
View 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>

7
views/user/user.etlua Normal file
View File

@ -0,0 +1,7 @@
<% if just_logged_in then %>
<h1>Logged in successfully.</h1>
<% end %>
<h1><%= user.username %></h1>
<% if user:is_guest() then %>
<h2>You are a guest. An administrator needs to approve your account before you will be able to post.</h2>
<% end %>