login

<     >

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>