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]+)/([ab])",
function (req, name, page)
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
if page == "b" then
ticks = self.model:get_backlog_tickets(proj.id)
else -- "a"
ticks = self.model:get_archived_tickets(proj.id)
end
local env = {title=self.title, user=user, proj=proj, page=page, ticks=ticks}
return {fname="view/inactive.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/(%w+)",
function (req, name, tcode, direction)
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 state_id = tick.state_id
if direction == "left" then
state_id = state_id - 1
elseif direction == "right" then
state_id = state_id + 1
else
self:log(LOG_ERROR, "invalid shift direction: "..direction)
return "invalid shift direction", 500
end
if state_id == 0 or state_id > #self.model.states then
self.model:archive_ticket(tick.id)
self:log(LOG_INFO, "user "..user.nick.." archived ticket "..name.."#"..tcode)
else
self.model:shift_ticket(user.id, tick, state_id)
end
local path = "/"
if req.query.from == "proj" then
path = "/p/"..name
elseif req.query.from == "tick" then
path = "/p/"..name.."/t/"..tcode
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)
local page
if tick.state_id == 1 then
page = "b" -- backlog
else
page = "a" -- archive
end
return "/p/"..name.."/"..page, 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()