2024-10-30 01:00:27 +00:00
|
|
|
#!/usr/bin/env luajit
|
|
|
|
|
|
|
|
local function escape(s)
|
|
|
|
return s:gsub("<", "<"):gsub("<", "<")
|
|
|
|
end
|
2024-10-30 12:50:36 +00:00
|
|
|
local function urldecode(s)
|
|
|
|
if s == nil then return nil end
|
|
|
|
return s:gsub("+", " "):gsub("%%20", " ")
|
|
|
|
end
|
|
|
|
local function urlencode(s)
|
|
|
|
if s == nil then return nil end
|
|
|
|
return s:gsub(" ", "%%20")
|
|
|
|
end
|
|
|
|
|
|
|
|
local function parse_query(q)
|
|
|
|
if q == nil then return {} end
|
|
|
|
local data = {}
|
|
|
|
for pair in string.gmatch(q, "([^&]+)") do
|
|
|
|
local flag = string.match(pair, "^([^=]+)$")
|
|
|
|
if flag ~= nil then
|
|
|
|
data[flag] = "1"
|
|
|
|
else
|
|
|
|
local key, value = string.match(pair, "^([^=]+)=([^=]*)$")
|
|
|
|
if key ~= nil and value ~= nil then
|
|
|
|
data[key] = urldecode(value)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return data
|
|
|
|
end
|
|
|
|
|
2024-10-30 22:09:58 +00:00
|
|
|
local function load_config()
|
|
|
|
local log = io.open("config", "r")
|
|
|
|
if log == nil then return {} end
|
|
|
|
local config = {}
|
|
|
|
for l in log:lines("l") do
|
|
|
|
if l ~= "" and l[0] ~= "#" then
|
|
|
|
local key, value = string.match(l, "^([^=]+)=([^=]*)")
|
|
|
|
if key ~= nil and value ~= nil then
|
|
|
|
config[key] = value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return config
|
|
|
|
end
|
|
|
|
|
|
|
|
local config = load_config()
|
2024-10-30 12:50:36 +00:00
|
|
|
local path = os.getenv("PATH_INFO")
|
|
|
|
local method = os.getenv("REQUEST_METHOD")
|
|
|
|
local query = parse_query(os.getenv("QUERY_STRING"))
|
|
|
|
|
|
|
|
local stylesheet = [[
|
|
|
|
/* body { background-color: #161616; }
|
|
|
|
h1, h2, h3, h4, h5, h6, p, label, a { color: #e2e2e2; } */
|
|
|
|
.amount-presets form { display: inline-block; width: 60px }
|
|
|
|
.amount-pos { color: green; }
|
|
|
|
.amount-neg { color: red; }
|
|
|
|
nav h2 { display: inline-block }
|
|
|
|
.notif { padding: 0.5em; margin: 0.5em; background-color: #ddd; }
|
2024-10-30 21:00:15 +00:00
|
|
|
.notif.error { background-color: #faa; }
|
2024-10-30 13:15:18 +00:00
|
|
|
.notif p { margin: 5px; }
|
|
|
|
form.box { border: 2px solid grey; padding: 0.5em; margin: 0.5em; display: inline-block; }
|
|
|
|
form h3 { margin: 5px; }
|
2024-10-30 12:50:36 +00:00
|
|
|
]]
|
|
|
|
|
|
|
|
local script = [[
|
2024-10-30 21:00:15 +00:00
|
|
|
document.addEventListener("keypress", ev => {
|
|
|
|
if (!(document.activeElement instanceof HTMLInputElement)) {
|
|
|
|
if (ev.code.startsWith("Digit"))
|
|
|
|
document.forms.buy_product.product.value += ev.code.substring(5)
|
|
|
|
if (ev.code == "Enter")
|
|
|
|
document.forms.buy_product.submit()
|
|
|
|
}
|
|
|
|
})
|
2024-10-30 12:50:36 +00:00
|
|
|
]]
|
2024-10-30 01:00:27 +00:00
|
|
|
|
|
|
|
local function respond(status, title, body)
|
|
|
|
print(string.format("Status: %d", status))
|
|
|
|
print("Content-Type: text/html")
|
|
|
|
print("")
|
|
|
|
print(string.format([[
|
2024-10-30 21:00:15 +00:00
|
|
|
<!DOCTYPE html>
|
2024-10-30 01:00:27 +00:00
|
|
|
<html><head>
|
|
|
|
<title>%s</title>
|
|
|
|
<meta charset="utf-8" />
|
2024-10-30 12:50:36 +00:00
|
|
|
<style>%s</style>
|
|
|
|
<script>%s</script>
|
2024-10-30 22:09:58 +00:00
|
|
|
%s
|
2024-10-30 12:50:36 +00:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<nav>
|
|
|
|
<h2><a href="/">Strichliste v2</a></h2>
|
|
|
|
<span><a href="/?log">View Log</a></span>
|
|
|
|
<span><a href="https://codeberg.org/metamuffin/strichliste">Source</a></span>
|
|
|
|
</nav>
|
2024-10-30 22:09:58 +00:00
|
|
|
]], escape(title), stylesheet, script, config.head_extra or ""))
|
2024-10-30 01:00:27 +00:00
|
|
|
body()
|
|
|
|
print("</body></html>")
|
|
|
|
end
|
|
|
|
|
|
|
|
local function respond_error(message)
|
|
|
|
respond(400, "Error", function()
|
|
|
|
print(string.format("<p>Error: %s</p>", escape(message)))
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2024-10-30 11:20:03 +00:00
|
|
|
local function redirect(path)
|
|
|
|
print("Status: 307")
|
|
|
|
print(string.format("Location: %s", path))
|
|
|
|
print()
|
|
|
|
end
|
|
|
|
|
2024-10-30 01:00:27 +00:00
|
|
|
local function form_data()
|
2024-10-30 12:50:36 +00:00
|
|
|
return parse_query(io.read())
|
2024-10-30 01:00:27 +00:00
|
|
|
end
|
|
|
|
|
2024-10-30 21:00:15 +00:00
|
|
|
local function format_duration(t)
|
2024-10-31 00:44:12 +00:00
|
|
|
if t > 86400 then return string.format("%d day%s", t / 86400, math.floor(t / 86400) ~= 1 and "s" or "") end
|
|
|
|
if t > 3600 then return string.format("%d hour%s", t / 3600, math.floor(t / 3600) ~= 1 and "s" or "") end
|
|
|
|
if t > 60 then return string.format("%d minute%s", t / 60, math.floor(t / 60) ~= 1 and "s" or "") end
|
2024-10-30 21:00:15 +00:00
|
|
|
return string.format("%d seconds", t)
|
|
|
|
end
|
|
|
|
|
2024-10-30 01:00:27 +00:00
|
|
|
local function read_log()
|
|
|
|
local log = io.open("log", "r")
|
|
|
|
if log == nil then
|
|
|
|
return function() return nil end
|
|
|
|
end
|
|
|
|
local lines = log:lines("l")
|
|
|
|
return function()
|
|
|
|
local l = lines()
|
2024-10-30 12:50:36 +00:00
|
|
|
if l == "" or l == nil then
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
local time, username, amount, comment = string.match(l, "(%d+),([%w_ -]+),(-?%d+),([%w_ -]*)")
|
2024-10-30 01:00:27 +00:00
|
|
|
return tonumber(time), username, tonumber(amount), comment
|
|
|
|
end
|
|
|
|
end
|
2024-10-30 12:50:36 +00:00
|
|
|
local function read_products()
|
|
|
|
local log = io.open("products", "r")
|
|
|
|
if log == nil then
|
|
|
|
return function() return nil end
|
|
|
|
end
|
|
|
|
local lines = log:lines("l")
|
|
|
|
return function()
|
|
|
|
local l = lines()
|
|
|
|
if l == "" or l == nil then
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
local barcode, amount, name = string.match(l, "([%w_-]+),(-?%d+),([%w_ -]*)")
|
|
|
|
return barcode, tonumber(amount), name
|
|
|
|
end
|
|
|
|
end
|
2024-10-30 01:00:27 +00:00
|
|
|
|
2024-10-30 01:11:09 +00:00
|
|
|
local function balances()
|
|
|
|
local users = {}
|
|
|
|
for _, username, amount, _ in read_log() do
|
|
|
|
users[username] = (users[username] or 0) + amount
|
|
|
|
end
|
|
|
|
return users
|
|
|
|
end
|
2024-10-30 21:00:15 +00:00
|
|
|
local function last_txns()
|
|
|
|
local users = {}
|
|
|
|
for time, username, _, _ in read_log() do
|
|
|
|
users[username] = time
|
2024-10-30 01:00:27 +00:00
|
|
|
end
|
2024-10-30 21:00:15 +00:00
|
|
|
return users
|
|
|
|
end
|
|
|
|
|
|
|
|
local function error_box(message)
|
|
|
|
return string.format([[<div class="notif error"><p>Error: %s</p></div>]], message)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function r_user_post(username)
|
|
|
|
local data = form_data()
|
|
|
|
local amount = nil
|
|
|
|
local comment = ""
|
|
|
|
if data.product then
|
|
|
|
for p_barcode, p_amount, p_name in read_products() do
|
|
|
|
if p_barcode == data.product then
|
|
|
|
amount = p_amount
|
|
|
|
comment = p_name
|
2024-10-30 12:50:36 +00:00
|
|
|
end
|
|
|
|
end
|
2024-10-30 01:00:27 +00:00
|
|
|
if amount == nil then
|
2024-10-30 21:00:15 +00:00
|
|
|
return error_box("unknown product")
|
2024-10-30 01:00:27 +00:00
|
|
|
end
|
2024-10-30 21:00:15 +00:00
|
|
|
else
|
|
|
|
amount = tonumber(data.amount)
|
|
|
|
comment = data.comment or ""
|
2024-10-30 01:00:27 +00:00
|
|
|
end
|
2024-10-30 21:00:15 +00:00
|
|
|
if amount == nil then
|
|
|
|
return error_box("amount invalid")
|
|
|
|
end
|
|
|
|
if comment:match("^[%w_ -]*$") == nil then
|
|
|
|
return error_box("comment invalid")
|
|
|
|
end
|
|
|
|
local log = io.open("log", "a+")
|
|
|
|
if log == nil then
|
|
|
|
return error_box("failed to open log")
|
|
|
|
end
|
|
|
|
local time = os.time()
|
|
|
|
log:write(string.format("%d,%s,%d,%s\n", time, username, amount, comment))
|
|
|
|
log:flush()
|
|
|
|
log:close()
|
2024-10-30 22:09:58 +00:00
|
|
|
return string.format([[
|
2024-10-30 23:19:10 +00:00
|
|
|
<div class="notif"><p>Transaction successful: <strong class="amount-%s">%.02f€</strong> (%s)</p></div>
|
|
|
|
<audio src="%s" autoplay></audio>
|
|
|
|
]],
|
2024-10-30 22:09:58 +00:00
|
|
|
amount >= 0 and "pos" or "neg", amount / 100,
|
|
|
|
escape(comment),
|
|
|
|
config.transaction_sound or ""
|
2024-10-30 21:00:15 +00:00
|
|
|
)
|
|
|
|
end
|
2024-10-30 01:00:27 +00:00
|
|
|
|
2024-10-30 21:00:15 +00:00
|
|
|
local function r_user(username)
|
|
|
|
local notif = nil
|
|
|
|
if method == "POST" then
|
|
|
|
notif = r_user_post(username)
|
|
|
|
end
|
2024-10-30 11:20:03 +00:00
|
|
|
return respond(200, username, function()
|
2024-10-30 01:00:27 +00:00
|
|
|
print(string.format("<h1>%s</h1>", username))
|
2024-10-30 12:50:36 +00:00
|
|
|
local balance = balances()[username]
|
2024-10-30 21:00:15 +00:00
|
|
|
local last_txn = last_txns()[username]
|
2024-10-30 12:50:36 +00:00
|
|
|
local new_user = balance == nil
|
|
|
|
balance = balance or 0
|
2024-10-30 22:09:58 +00:00
|
|
|
if notif then print(notif) end
|
2024-10-30 12:50:36 +00:00
|
|
|
if new_user then
|
|
|
|
print([[
|
|
|
|
<div class="notif"><p><i>This user account does not exist yet. It will only be created after the first transaction.</i></p></div>
|
|
|
|
]])
|
2024-10-30 22:09:58 +00:00
|
|
|
else
|
|
|
|
print(string.format([[
|
|
|
|
<p>Current balance: <span class="amount-%s">%.02f€</p>
|
|
|
|
]], balance >= 0 and "pos" or "neg", balance / 100))
|
|
|
|
print(string.format([[
|
|
|
|
<p>Last transaction added %s ago. <a href="/%s?log">View user log</a>
|
|
|
|
]], format_duration(os.time() - last_txn), username))
|
2024-10-30 12:50:36 +00:00
|
|
|
end
|
2024-10-30 01:00:27 +00:00
|
|
|
print([[
|
2024-10-30 13:15:18 +00:00
|
|
|
<form class="transaction box" action="" method="POST">
|
|
|
|
<h3>Create Transaction</h3>
|
2024-10-30 01:00:27 +00:00
|
|
|
<label for="amount">Amount: </label>
|
|
|
|
<input type="number" name="amount" id="amount" /><br/>
|
|
|
|
<label for="comment">Comment: </label>
|
|
|
|
<input type="text" name="comment" id="comment" /><br/>
|
|
|
|
<input type="submit" value="Update" />
|
|
|
|
</form>
|
2024-10-30 21:00:15 +00:00
|
|
|
<form class="transaction box" action="" method="POST" id="buy_product">
|
|
|
|
<h3>Buy Product</h3>
|
|
|
|
<label for="product">Product: </label>
|
|
|
|
<input type="text" name="product" id="product" /><br/>
|
|
|
|
<input type="submit" value="Buy" />
|
|
|
|
</form>
|
2024-10-30 01:00:27 +00:00
|
|
|
]])
|
2024-10-31 00:44:12 +00:00
|
|
|
print([[<div class="amount-presets">]])
|
2024-10-30 01:00:27 +00:00
|
|
|
for _, type in ipairs({ 1, -1 }) do
|
|
|
|
for _, amount in ipairs({ 50, 100, 150, 200, 500, 1000 }) do
|
|
|
|
print(string.format([[
|
|
|
|
<form action="" method="POST">
|
|
|
|
<input type="number" name="amount" id="amount" value="%d" hidden />
|
|
|
|
<input type="text" name="comment" id="comment" value="" hidden />
|
2024-10-30 12:50:36 +00:00
|
|
|
<input type="submit" value="%s%.02f€" class="amount-%s" />
|
2024-10-30 01:00:27 +00:00
|
|
|
</form>
|
2024-10-30 12:50:36 +00:00
|
|
|
]], amount * type, ({ [-1] = "-", [1] = "+" })[type], amount / 100,
|
|
|
|
({ [-1] = "neg", [1] = "pos" })[type]))
|
2024-10-30 01:00:27 +00:00
|
|
|
end
|
2024-10-30 12:50:36 +00:00
|
|
|
print("<br/>")
|
2024-10-30 01:00:27 +00:00
|
|
|
end
|
2024-10-30 12:50:36 +00:00
|
|
|
print("</div>")
|
2024-10-30 01:00:27 +00:00
|
|
|
end)
|
|
|
|
end
|
2024-10-30 11:20:03 +00:00
|
|
|
|
2024-10-30 21:00:15 +00:00
|
|
|
local function r_log(filter)
|
2024-10-30 11:20:03 +00:00
|
|
|
return respond(200, "Log", function()
|
|
|
|
print("<table>")
|
|
|
|
print("<tr><th>Time</th><th>Username</th><th>Amount</th><th>Comment</th></tr>")
|
|
|
|
for time, username, amount, comment in read_log() do
|
2024-10-30 21:00:15 +00:00
|
|
|
if filter == nil or filter == username then
|
2024-10-30 23:19:10 +00:00
|
|
|
print(string.format([[
|
|
|
|
<tr>
|
|
|
|
<td>%d (%s ago)</td>
|
|
|
|
<td>%s</td>
|
2024-10-31 00:44:12 +00:00
|
|
|
<td class="amount-%s">%.02f€</td>
|
2024-10-30 23:19:10 +00:00
|
|
|
<td>%s</td>
|
|
|
|
<td>
|
|
|
|
<form action="/%s" method="POST">
|
|
|
|
<input type="number" name="amount" id="amount" value="%d" hidden />
|
|
|
|
<input type="text" name="comment" id="comment" value="Revert %s" hidden />
|
|
|
|
<input type="submit" value="Revert" />
|
|
|
|
</form>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
]],
|
2024-10-30 21:00:15 +00:00
|
|
|
time, format_duration(os.time() - time),
|
|
|
|
escape(username),
|
|
|
|
amount >= 0 and "pos" or "neg", amount / 100,
|
2024-10-30 23:19:10 +00:00
|
|
|
escape(comment),
|
|
|
|
escape(username),
|
|
|
|
-amount,
|
2024-10-30 21:00:15 +00:00
|
|
|
escape(comment)
|
|
|
|
))
|
|
|
|
end
|
2024-10-30 11:20:03 +00:00
|
|
|
end
|
|
|
|
print("</table>")
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function r_index()
|
|
|
|
return respond(200, "Users", function()
|
|
|
|
print([[
|
2024-10-30 13:15:18 +00:00
|
|
|
<form action="/" method="GET" class="box">
|
|
|
|
<h3>User Creation</h3>
|
2024-10-30 11:20:03 +00:00
|
|
|
<label for="username">Username: </label>
|
|
|
|
<input type="text" name="create_user" id="username" /><br/>
|
2024-10-30 12:50:36 +00:00
|
|
|
<input type="submit" value="Continue" />
|
2024-10-30 11:20:03 +00:00
|
|
|
</form>
|
|
|
|
]])
|
|
|
|
print("<ul>")
|
|
|
|
for username, balance in pairs(balances()) do
|
2024-10-31 00:44:12 +00:00
|
|
|
print(string.format([[
|
|
|
|
<li><a href="/%s">%s</a>: <span class="amount-%s">%.02f€</span></li>
|
|
|
|
]],
|
|
|
|
urlencode(username),
|
|
|
|
escape(username),
|
|
|
|
balance >= 0 and "pos" or "neg", balance / 100
|
|
|
|
))
|
2024-10-30 11:20:03 +00:00
|
|
|
end
|
|
|
|
print("</ul>")
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function r_create_user()
|
2024-10-30 12:50:36 +00:00
|
|
|
local username = query.create_user
|
|
|
|
if username:match("^([%w_ -]+)$") == nil then
|
|
|
|
return respond_error("invalid username " .. username)
|
2024-10-30 11:20:03 +00:00
|
|
|
end
|
2024-10-30 12:50:36 +00:00
|
|
|
return redirect(string.format("/%s", urlencode(username)))
|
2024-10-30 11:20:03 +00:00
|
|
|
end
|
|
|
|
|
2024-10-30 21:00:15 +00:00
|
|
|
local function extract_username()
|
|
|
|
if path == nil then
|
|
|
|
return respond_error("no path")
|
|
|
|
end
|
|
|
|
local username = urldecode(path:sub(2))
|
|
|
|
if username == nil or username:match("^([%w_ -]+)$") == nil then
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
return username
|
|
|
|
end
|
|
|
|
|
2024-10-30 11:20:03 +00:00
|
|
|
if path == "/" then
|
2024-10-30 12:50:36 +00:00
|
|
|
if query.log then
|
2024-10-30 11:20:03 +00:00
|
|
|
return r_log()
|
2024-10-30 12:50:36 +00:00
|
|
|
elseif query.create_user then
|
2024-10-30 11:20:03 +00:00
|
|
|
return r_create_user()
|
|
|
|
else
|
|
|
|
return r_index()
|
|
|
|
end
|
|
|
|
else
|
2024-10-30 21:00:15 +00:00
|
|
|
local username = extract_username()
|
|
|
|
if username == nil then
|
|
|
|
return respond_error("username invalid")
|
|
|
|
elseif query.log then
|
|
|
|
return r_log(username)
|
|
|
|
else
|
|
|
|
return r_user(username)
|
|
|
|
end
|
2024-10-30 11:20:03 +00:00
|
|
|
end
|