starting users
This commit is contained in:
parent
03a20128f7
commit
ac51e5c0e8
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ logs/
|
||||
nginx.conf.compiled
|
||||
db.*.sqlite
|
||||
.vscode/
|
||||
.local/
|
||||
|
5
app.lua
5
app.lua
@ -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
181
apps/users.lua
Normal 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
|
@ -8,4 +8,5 @@ config("development", {
|
||||
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()
|
18
migrations.lua
Normal file
18
migrations.lua
Normal 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
|
||||
}
|
23
models.lua
23
models.lua
@ -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
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>
|
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>
|
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>
|
7
views/user/user.etlua
Normal file
7
views/user/user.etlua
Normal 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 %>
|
Loading…
Reference in New Issue
Block a user