2023-07-22 19:07:00 (UTC-03:00)
Marcel Rodrigues <marcelgmr@gmail.com>
add app with user creation process
diff --git a/auth.lua b/auth.lua new file mode 100644 index 0000000..f091163 --- /dev/null +++ b/auth.lua @@ -0,0 +1,14 @@ +local lud = require "ludweb" + +local function get_salt() + return lud.crypt.urandom(32) +end + +local function hash_pass(pass, salt) + return lud.crypt.pbkdf2(pass, salt, 10000, 64) +end + +return { + b64_enc=lud.crypt.b64_enc, b64_dec=lud.crypt.b64_dec, hex=lud.crypt.hex, + uuid4=lud.crypt.uuid4, get_salt=get_salt, hash_pass=hash_pass +} diff --git a/data.lua b/data.lua index 0bba356..35a4ce1 100644 --- a/data.lua +++ b/data.lua @@ -1,5 +1,7 @@ local lud = require "ludweb" +local auth = require "auth" + local schema = [[ CREATE TABLE IF NOT EXISTS User ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -92,13 +94,17 @@ function Model:get_comments(ticket_id) return self.db:execute("SELECT * FROM Comment WHERE ticket_id = ?;", ticket_id) end -function Model:create_user(nick, name, salt, hash) +function Model:create_user(nick, name, password) + local salt = auth.get_salt() + local hash = auth.hash_pass(password, salt) + salt = auth.b64_enc(salt) + hash = auth.b64_enc(hash) local query = "INSERT INTO User(nick, name, salt, hash) VALUES (?, ?, ?, ?);" self.db:execute(query, nick, name, salt, hash) end function Model:create_invite() - local uuid = lud.crypt.uuid4() + local uuid = auth.hex(auth.uuid4()) local expire = "unixepoch('now', '+2 days')" self.db:execute("INSERT INTO Invite(uuid, expire) VALUES (?, "..expire..");", uuid) return uuid diff --git a/skopos.lua b/skopos.lua index 6ef39c4..dbba0cc 100644 --- a/skopos.lua +++ b/skopos.lua @@ -1,9 +1,117 @@ local lud = require "ludweb" local data = require "data" +local auth = require "auth" -local db_path = ":memory:" +local LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG = 0, 1, 2, 3 -local db = lud.sqlite.open(db_path) -db:execute_many(data.schema) -db:close() +local App = {} +App.__index = App + +function App:init() + self.model:create_tables() + if #self.model:get_states() == 0 then + self.model:create_states() + end + local uuid = self.model:create_invite() + self:log(LOG_INFO, "root invite: "..uuid) +end + +function App:run() + self:log(LOG_INFO, "server running on port "..self.port) + self.app:run(self.port) +end + +function App:log(level, msg) + local level_str = {"ERROR", "WARN", "INFO", "DEBUG"} + if self.log_level >= level then + io.stderr:write(("[%s] %s\n"):format(level_str[level+1], msg)) + end +end + +function App:routes() + return { + {"GET", "/?", + function (req) + local session_id = req.cookies.sid + local user + if session_id ~= nil then + user = self.model:get_user(self.sessions[session_id]) + end + local env = {title=self.title, user=user} + return lud.template.render_file("view/home.html", env) + end}, + {"GET", "/join", + function (req) + local uuid = req.query.invite or "" + local env = {title=self.title, uuid=uuid} + return lud.template.render_file("view/join.html", env) + end}, + {"POST", "/join", + function (req) + local uuid = req.form.invite + local nick = req.form.username + local name = req.form.realname + local pass = req.form.password + local fail_path = "/join?invite="..uuid + if #pass == 0 then -- empty password + return fail_path, 303 + end + if self.model:get_user(nick) ~= nil then -- user already exists + return fail_path, 303 + end + if not self.model:use_invite(uuid) then -- invalid/expired invite + self:log(LOG_WARN, "attempt to use invalid invite: "..uuid) + return fail_path, 303 + end + self.model:create_user(nick, name, pass) + self:log(LOG_INFO, "new user joined: "..nick) + return "/login", 303 + end}, + {"GET", "/login", + function (req) + local env = {title=self.title} + return lud.template.render_file("view/login.html", env) + end}, + {"POST", "/login", + function (req) + local nick = req.form.username + local pass = req.form.password + local user = self.model:get_user(nick) + if user == nil then + self:log(LOG_WARN, "invalid username: "..nick) + return "/login", 303 + end + local salt = auth.b64_dec(user.salt) + local hash = auth.hash_pass(pass, salt) + if hash == auth.b64_dec(user.hash) then + local session_id = auth.b64_enc(auth.uuid4()) + self.sessions[session_id] = nick + self:log(LOG_INFO, "logged in as "..nick) + local cookie = {key="sid", val=session_id, path="/", age=3*24*60*60} + return "/", 303, "See Other", {cookie} + else + self:log(LOG_WARN, "invalid password for "..nick) + end + return "/login", 303 + end}, +} end + +local function new_app(db_path, port, title, log_level) + local log_levels = {ERROR=0, WARN=1, INFO=2, DEBUG=3} + db_path = db_path or ":memory:" + local self = { + port=tonumber(port) or 8080, + title=title or "skopos", + log_level=log_levels[log_level or "INFO"], + sessions={}, + } + local self = setmetatable(self, App) + self.model = data.open(db_path) + self.app = lud.app.new_app(self:routes()) + self:init() + return self +end + +local app = new_app() +app:run() diff --git a/view/form.css b/view/form.css new file mode 100644 index 0000000..b3c35ba --- /dev/null +++ b/view/form.css @@ -0,0 +1,34 @@ + .ul-form { + padding: 0; + list-style-type: none; + } + .flat-field { + margin: 5px; + font-size: 18px; + background: white; + border-width: 1px; + border-style: solid; + border-color: #B4C2AA; + border-radius: 0px; + } + .flat-field:invalid { + border-color: lightcoral; + } + .flat-button { + margin: 10px; + box-shadow: none; + background: none; + background-color: #F5FFE2; + border-radius: 6px; + border: 3px solid #B4C2AA; + font-weight: bold; + padding: 5px 20px; + cursor: default; + color: black; + font-size: 16px; + text-decoration: none; + text-shadow: 1px 1px 0px white; + } + .flat-button:hover { + background-color: var(--color-2); + } diff --git a/view/home.html b/view/home.html new file mode 100644 index 0000000..35d8f35 --- /dev/null +++ b/view/home.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>{{$title}}</title> + <style> + .centered { text-align: center; } + </style> +</head> +<body> + <h1 class="centered">Home</h1> + % if $user ~= nil then + <p>Welcome {{$user.name}}!</p> + % else + <p>Please <a href="/login">log in</a>.</p> + % end +</body> +</html> diff --git a/view/join.html b/view/join.html new file mode 100644 index 0000000..fad5354 --- /dev/null +++ b/view/join.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>{{$title}} - join</title> + <style> + .centered { text-align: center; } + % include view/form.css + </style> +</head> +<body> + <h1 class="centered">Join</h1> + <form action="/join" method="post"> + <ul class="centered ul-form"> + % if #$uuid == 0 then + % set iaf = "autofocus" + % set uaf = "" + % else + % set iaf = "" + % set uaf = "autofocus" + % end + <li><input type="text" class="flat-field" name="invite" placeholder="Invite" value="{{$uuid}}" {{$iaf}}></li> + <li><input type="text" class="flat-field" name="username" placeholder="Username" {{$uaf}}></li> + <li><input type="text" class="flat-field" name="realname" placeholder="Real Name"></li> + <li><input type="password" class="flat-field" name="password" placeholder="Password"></li> + <li><input type="submit" class="flat-button" value="Join"></li> + </ul> + </form> +</body> +</html> diff --git a/view/login.html b/view/login.html new file mode 100644 index 0000000..9554310 --- /dev/null +++ b/view/login.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>{{$title}} - login</title> + <style> + .centered { text-align: center; } + % include view/form.css + </style> +</head> +<body> + <h1 class="centered">Login</h1> + <form action="/login" method="post"> + <ul class="centered ul-form"> + <li><input type="text" class="flat-field" name="username" placeholder="Username" autofocus></li> + <li><input type="password" class="flat-field" name="password" placeholder="Password"></li> + <li><input type="submit" class="flat-button" value="Login"></li> + </ul> + </form> +</body> +</html>