login

local lud = require "ludweb"

local data = require "data"
local auth = require "auth"

local LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG = 0, 1, 2, 3

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()