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
io.stderr:write(("[%s] %s\n"):format(level_str[level+1], 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
-- TODO: set proj_id
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 lud.template.render_file("view/board.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: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}
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)
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 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
end
return "/login", 303
end},
{"GET", "/i",
function (req)
local user = self:get_user(req)
if user == nil then return "/login", 303 end
local invites = self.model:get_invites(user.id)
local env = {title=self.title, invites=invites}
return lud.template.render_file("view/invites.html", 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},
{"GET", "/p",
function (req)
local user = self:get_user(req)
if user == nil then return "/login", 303 end
local projs = self.model:get_user_projects(user.id)
local env = {title=self.title, user=user, projs=projs}
return lud.template.render_file("view/projs.html", env)
end},
{"GET", "/p/new",
function (req)
local user = self:get_user(req)
if user == nil then return "/login", 303 end
local env = {title=self.title, user=user}
return lud.template.render_file("view/proj_form.html", env)
end},
{"GET", "/p/([-_%w]+)",
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 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 lud.template.render_file("view/board.html", 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", 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 lud.template.render_file("view/proj_form.html", 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},
{"GET", "/p/([-_%w]+)/t/new",
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 env = {title=self.title, user=user, proj=proj}
return lud.template.render_file("view/ticket_form.html", env)
end},
{"GET", "/p/([-_%w]+)/t/(%d+)",
function (req, name, code)
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, code, true)
if tick == nil then return "not found", 404 end
-- TODO: load comments
local comments = {}
local env = {
title=self.title, user=user, proj=proj, tick=tick,
states=self.model.states, comments=comments
}
return lud.template.render_file("view/ticket.html", 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 code = self.model:create_ticket(user.id, proj.id, title, desc, priority)
self:log(LOG_INFO, "user "..user.nick.." created ticket "..name.."#"..code)
return "/p/"..name.."/t/"..code, 303
end},
{"GET", "/p/([-_%w]+)/t/(%d+)/edit",
function (req, name, code)
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, code)
if tick == nil then return "not found", 404 end
local env = {title=self.title, user=user, proj=proj, tick=tick}
return lud.template.render_file("view/ticket_form.html", env)
end},
{"POST", "/p/([-_%w]+)/t/(%d+)/put",
function (req, name, code)
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, code)
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.."#"..code)
return "/p/"..name.."/t/"..code, 303
end},
{"POST", "/p/([-_%w]+)/t/(%d+)/shift/(%d+)",
function (req, name, code, 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, code)
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
else
path = "/"
end
return path, 303
end},
{"POST", "/p/([-_%w]+)/t/(%d+)/del",
function (req, name, code)
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, code)
if tick ~= nil then
self.model:del_ticket(tick.id)
self:log(LOG_INFO, "user "..user.nick.." deleted ticket "..name.."#"..code)
end
return "/p/"..name, 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()