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()
    local user_count = self.model:get_user_count()
    if user_count == 0 then
        local uuid = self.model:create_invite()
        self:log(LOG_INFO, "root invite: "..uuid)
    else
        self.model:expire_invites()
        self:log(LOG_INFO, "user count: "..user_count)
    end
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
        local time_s = os.date("%F %T")
        local level_s = level_str[level+1]
        io.stderr:write(("[%s %s] %s\n"):format(time_s, level_s, msg))
    end
end

function App:get_user(req)
    local session_id = req.cookies.sid
    local user
    if session_id ~= nil then
        user = self.model:get_user(self.sessions[session_id])
    end
    return user
end

function App:routes()
    return {
    {"GET", "/?",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local states = self.model.states
        local columns = self.model:get_board(user.id)
        local env = {title=self.title, user=user, states=states, columns=columns}
        return {fname="view/board.html", env=env}
    end},
    {"GET", "/join",
    function (req)
        local uuid = req.query.invite or ""
        local env = {title=self.title, uuid=uuid}
        return {fname="view/join.html", env=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 pass ~= req.form.passconfirm then -- invalid password confirmation
            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:create_user(nick, name, pass, uuid) then
            self:log(LOG_WARN, "attempt to use invalid invite: "..uuid)
            return fail_path, 303
        end
        self:log(LOG_INFO, "new user joined: "..nick)
        return "/login", 303
    end},
    {"GET", "/login",
    function (req)
        local env = {title=self.title, after=req.query.after}
        return {fname="view/login.html", env=env}
    end},
    {"POST", "/login",
    function (req)
        local nick = req.form.username
        local pass = req.form.password
        local user = self.model:get_user(nick)
        local salt, hash
        if user == nil then
            -- hash something as if we're trying to login anyway
            salt = auth.get_salt()
            hash = auth.hash_pass(pass, salt)
            self:log(LOG_WARN, "invalid username: "..nick)
        else
            salt = auth.b64_dec(user.salt)
            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 path = req.query.after or "/"
                local cookie = {key="sid", val=session_id, path="/", age=3*24*60*60}
                return path, 303, "See Other", {cookie}
            else
                self:log(LOG_WARN, "invalid password for "..nick)
            end
        end
        return "/login", 303
    end},
    {"GET", "/logout",
    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
        if user ~= nil then
            self:log(LOG_INFO, "logged out as "..user.nick)
            self.sessions[session_id] = nil
        end
        return "/login", 303
    end},
    {"GET", "/u/([-_%w]+)",
    function (req, nick)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local other = self.model:get_user(nick, true)
        if other == nil then return "not found", 404 end
        local invitees = self.model:get_invitees(other.id)
        local manage = self.model:can_manage(user, other)
        local projs = self.model:get_user_projects(other.id)
        local env = {
            title=self.title, user=user, other=other, invitees=invitees,
            manage=manage, projs=projs
        }
        return {fname="view/user.html", env=env}
    end},
    {"POST", "/u/([-_%w]+)/del",
    function (req, nick)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local other = self.model:get_user(nick)
        if other == nil then return "not found", 404 end
        if self.model:can_manage(user, other) then
            self.model:del_user(other.id)
            self:log(LOG_INFO, "user "..user.nick.." deleted user "..other.nick)
        end
        return "/", 303
    end},
    -- invites
    {"GET", "/i",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login?after=/i", 303 end
        local invites = self.model:get_invites(user.id)
        local env = {title=self.title, user=user, invites=invites}
        return {fname="view/invites.html", env=env}
    end},
    {"POST", "/i",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        self.model:create_invite(user.id)
        self:log(LOG_INFO, "user "..user.nick.." generated a new invite")
        return "/i", 303
    end},
    {"POST", "/i/([%x]+)/del",
    function (req, uuid)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        self.model:del_invite(user.id, uuid)
        self:log(LOG_INFO, "user "..user.nick.." canceled invite "..uuid)
        return "/i", 303
    end},
    -- projects
    {"GET", "/p",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login?after=/p", 303 end
        local projs = self.model:get_user_projects(user.id)
        local env = {title=self.title, user=user, projs=projs}
        return {fname="view/projs.html", env=env}
    end},
    {"GET", "/p/new",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login?after=/p/new", 303 end
        local env = {title=self.title, user=user}
        return {fname="view/proj_form.html", env=env}
    end},
    {"GET", "/p/([-_%w]+)",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local states = self.model.states
        local columns = self.model:get_board(user.id, proj.id)
        local env = {
            title=self.title, user=user, proj=proj,
            states=states, columns=columns
        }
        return {fname="view/board.html", env=env}
    end},
    {"GET", "/p/([-_%w]+)/a",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local ticks = self.model:get_archived_tickets(proj.id)
        local env = {title=self.title, user=user, proj=proj, ticks=ticks}
        return {fname="view/archive.html", env=env}
    end},
    {"POST", "/p",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local name = req.form.name
        local desc = req.form.desc
        local goal = req.form.goal
        local color = req.form.color
        local priority = tonumber(req.form.priority or 0)
        self.model:create_project(user.id, name, desc, goal, color, priority)
        self:log(LOG_INFO, "user "..user.nick.." created project "..name)
        return "/p", 303
    end},
    {"GET", "/p/([-_%w]+)/edit",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local env = {title=self.title, user=user, proj=proj}
        return {fname="view/proj_form.html", env=env}
    end},
    {"POST", "/p/([-_%w]+)/put",
    function (req, old_name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local new_name = req.form.name
        local desc = req.form.desc
        local goal = req.form.goal
        local color = req.form.color
        local priority = tonumber(req.form.priority or 0)
        self.model:update_project(old_name, new_name, desc, goal, color, priority)
        self:log(LOG_INFO, "user "..user.nick.." edited project "..old_name)
        return "/p", 303
    end},
    {"POST", "/p/([-_%w]+)/del",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_project(name)
        if proj ~= nil then
            self.model:del_project(proj.id)
            self:log(LOG_INFO, "user "..user.nick.." deleted project "..name)
        end
        return "/p", 303
    end},
    {"POST", "/p/([-_%w]+)/archive",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_project(name)
        if proj ~= nil and proj.is_active ~= 0 then
            self.model:archive_project(proj.id)
            self:log(LOG_INFO, "user "..user.nick.." archived project "..name)
        end
        return "/p", 303
    end},
    {"POST", "/p/([-_%w]+)/restore",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_project(name)
        if proj ~= nil and proj.is_active == 0 then
            self.model:restore_project(proj.id)
            self:log(LOG_INFO, "user "..user.nick.." restored project "..name)
        end
        return "/p", 303
    end},
    -- members
    {"GET", "/p/([-_%w]+)/m",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local members = self.model:get_members(proj.id)
        local rest = self.model:get_non_members(proj.id)
        local env = {
            title=self.title, user=user, proj=proj, members=members, rest=rest
        }
        return {fname="view/members.html", env=env}
    end},
    {"POST", "/p/([-_%w]+)/m/add",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local nick = req.form.username
        if self.model:add_member(proj.id, nick) then
            self:log(LOG_INFO, "user "..user.nick.." added "..nick.." to "..name)
        end
        return "/p/"..name.."/m", 303
    end},
    {"POST", "/p/([-_%w]+)/m/([-_%w]+)/rem",
    function (req, name, nick)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        if self.model:remove_member(proj.id, nick) then
            self:log(LOG_INFO, "user "..user.nick.." removed "..nick.." from "..name)
        end
        return "/p/"..name.."/m", 303
    end},
    -- tickets
    {"GET", "/p/([-_%w]+)/t/new",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local env = {title=self.title, user=user, proj=proj}
        return {fname="view/ticket_form.html", env=env}
    end},
    {"GET", "/p/([-_%w]+)/t/(%d+)",
    function (req, name, tcode)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode, true)
        if tick == nil then return "not found", 404 end
        local events = self.model:get_events(tick.id, true)
        local env = {
            title=self.title, user=user, proj=proj, tick=tick,
            states=self.model.states, events=events
        }
        return {fname="view/ticket.html", env=env}
    end},
    {"POST", "/p/([-_%w]+)/t",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local title = req.form.title
        local desc = req.form.desc
        local priority = tonumber(req.form.priority or 0)
        local tcode = self.model:create_ticket(user.id, proj.id, title, desc, priority)
        self:log(LOG_INFO, "user "..user.nick.." created ticket "..name.."#"..tcode)
        return "/p/"..name.."/t/"..tcode, 303
    end},
    {"GET", "/p/([-_%w]+)/t/(%d+)/edit",
    function (req, name, tcode)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        local env = {title=self.title, user=user, proj=proj, tick=tick}
        return {fname="view/ticket_form.html", env=env}
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/put",
    function (req, name, tcode)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        local title = req.form.title
        local desc = req.form.desc
        local priority = tonumber(req.form.priority or 0)
        self.model:update_ticket(tick.id, title, desc, priority)
        self:log(LOG_INFO, "user "..user.nick.." edited ticket "..name.."#"..tcode)
        return "/p/"..name.."/t/"..tcode, 303
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/shift/(%d+)",
    function (req, name, tcode, state_id)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        self.model:shift_ticket(user.id, tick, tonumber(state_id))
        local path = "/"
        if req.query.from == "proj" then
            path = "/p/"..name
        end
        return path, 303
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/archive",
    function (req, name, tcode)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        self.model:archive_ticket(tick.id)
        self:log(LOG_INFO, "user "..user.nick.." archived ticket "..name.."#"..tcode)
        local path = "/"
        if req.query.from == "proj" then
            path = "/p/"..name
        end
        return path, 303
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/restore",
    function (req, name, tcode)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        self.model:restore_ticket(tick.id)
        self:log(LOG_INFO, "user "..user.nick.." restored ticket "..name.."#"..tcode)
        return "/p/"..name.."/a", 303
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/del",
    function (req, name, tcode)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick ~= nil then
            self.model:del_ticket(tick.id)
            self:log(LOG_INFO, "user "..user.nick.." deleted ticket "..name.."#"..tcode)
        end
        return "/p/"..name, 303
    end},
    -- comments
    {"GET", "/p/([-_%w]+)/t/(%d+)/c/new",
    function (req, name, tcode)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        local env = {title=self.title, user=user, proj=proj, tick=tick}
        return {fname="view/comment_form.html", env=env}
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/c",
    function (req, name, tcode)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        local text = req.form.text
        local code = self.model:add_comment(user.id, tick.id, text)
        self:log(LOG_INFO, "user "..user.nick.." commented on ticket "..name.."#"..tcode)
        return "/p/"..name.."/t/"..tcode, 303
    end},
    {"GET", "/p/([-_%w]+)/t/(%d+)/c/(%d+)/edit",
    function (req, name, tcode, ccode)
        local user = self:get_user(req)
        if user == nil then return "/login?after="..req.path, 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        local comm = self.model:get_comment(tick.id, ccode)
        if comm == nil then return "not found", 404 end
        local env = {title=self.title, user=user, proj=proj, tick=tick, comm=comm}
        return {fname="view/comment_form.html", env=env}
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/c/(%d+)/put",
    function (req, name, tcode, ccode)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        local comm = self.model:get_comment(tick.id, ccode)
        if comm == nil then return "not found", 404 end
        local text = req.form.text
        self.model:update_comment(comm.id, text)
        self:log(LOG_INFO, "user "..user.nick.." edited comment "..name.."#"..tcode.."#"..ccode)
        return "/p/"..name.."/t/"..tcode, 303
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/c/(%d+)/del",
    function (req, name, tcode, ccode)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tcode)
        if tick == nil then return "not found", 404 end
        local comm = self.model:get_comment(tick.id, ccode)
        if comm ~= nil then
            self.model:del_comment(comm.id)
            self:log(LOG_INFO, "user "..user.nick.." deleted comment "..name.."#"..tcode.."#"..ccode)
        end
        return "/p/"..name.."/t/"..tcode, 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(unpack(arg))
app:run()