#!/usr/bin/env luajit local function escape(s) return s:gsub("<", "<"):gsub("<", "<") end 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 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() local path = os.getenv("PATH_INFO") local method = os.getenv("REQUEST_METHOD") local query = parse_query(os.getenv("QUERY_STRING")) local stylesheet = io.open("style.css"):read("a") local script = io.open("script.js"):read("a") local function respond(status, title, body) print(string.format("Status: %d", status)) print("Content-Type: text/html") print("") print(string.format([[ %s %s ]], escape(title), stylesheet, script, config.head_extra or "")) body() print("") end local function respond_error(message) respond(400, "Error", function() print(string.format("

Error: %s

", escape(message))) end) end local function redirect(path) print("Status: 307") print(string.format("Location: %s", path)) print() end local function form_data() return parse_query(io.read()) end local function format_duration(t) 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 return string.format("%d seconds", t) end 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() if l == "" or l == nil then return nil end local time, username, amount, comment = string.match(l, "(%d+),([%w_ -]+),(-?%d+),([%w_ -]*)") return tonumber(time), username, tonumber(amount), comment end end 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, price, name = string.match(l, "([%w_-]+),(-?%d+),([%w_ -]*)") return barcode, tonumber(price), name end end local function balances() local users = {} for _, username, amount, _ in read_log() do users[username] = (users[username] or 0) + amount end return users end local function last_txns() local users = {} for time, username, _, _ in read_log() do users[username] = time end return users end local function error_box(message) return string.format([[

Error: %s

]], 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 end end if amount == nil then return error_box("unknown product") end else amount = tonumber(data.amount) comment = data.comment or "" end 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() return string.format([[

Transaction successful: %.02f€ (%s)

]], amount >= 0 and "pos" or "neg", amount / 100, escape(comment), config.transaction_sound or "" ) end local function r_user(username) local notif = nil if method == "POST" then notif = r_user_post(username) end return respond(200, string.format("Abrechenbarheit: %s", username), function() print(string.format("

%s

", username)) local balance = balances()[username] local last_txn = last_txns()[username] local new_user = balance == nil balance = balance or 0 if notif then print(notif) end if new_user then print([[

This user account does not exist yet. It will only be created after the first transaction.

]]) else print(string.format([[

Current balance:%.02f€

]], balance >= 0 and "pos" or "neg", balance / 100)) print(string.format([[

Last transaction added %s ago. View user log ]], format_duration(os.time() - last_txn), username)) end print([[

]]) print([[
]]) for _, type in ipairs({ 1, -1 }) do for _, amount in ipairs({ 50, 100, 150, 200, 500, 1000 }) do print(string.format([[
]], amount * type, ({ [-1] = "-", [1] = "+" })[type], amount / 100, ({ [-1] = "neg", [1] = "pos" })[type])) end end print("
") print([[

Create Transaction

Buy Product

]]) print("
") end) end local function r_log(filter) return respond(200, "Abrechnungen", function() print("") print("") for time, username, amount, comment in read_log() do if filter == nil or filter == username then print(string.format([[ ]], time, format_duration(os.time() - time), escape(username), amount >= 0 and "pos" or "neg", amount / 100, escape(comment), escape(username), -amount, escape(comment) )) end end print("
TimeUsernameAmountComment
%d (%s ago) %s %.02f€ %s
") end) end local function r_index() return respond(200, "Abrechenbarkeit", function() print([[

User Creation

]]) print("") end) end local function r_create_user() local username = query.create_user if username:match("^([%w_ -]+)$") == nil then return respond_error("invalid username " .. username) end return redirect(string.format("/%s", urlencode(username))) end local function r_products_post() local data = form_data() local barcode = data.barcode if barcode == nil then return error_box("barcode unset") end if barcode:match("^[%w_-]*$") == nil then return error_box("barcode invalid") end if data.delete then local new_products = io.open("products.new", "w+") if new_products == nil then return error_box("failed to open new products") end for a_barcode, price, name in read_products() do if barcode ~= a_barcode then new_products:write(string.format("%s,%d,%s\n", a_barcode, price, name)) end end new_products:flush() new_products:close() os.rename("products.new", "products") else local price = tonumber(data.price) local name = data.name if price == nil then return error_box("price invalid") end if name:match("^[%w_ -]*$") == nil then return error_box("name invalid") end local products = io.open("products", "a+") if products == nil then return error_box("failed to open products") end products:write(string.format("%s,%d,%s\n", barcode, price, name)) products:flush() products:close() end end local function r_products() local notif = nil if method == "POST" then notif = r_products_post() end respond(200, "Abrechenbare Product List", function() print("

Product List

") if notif then print(notif) end print([[

Add Product

Remove Product

]]) print("") for barcode, price, name in read_products() do print(string.format([[ ]], name, price >= 0 and "pos" or "neg", price / 100, barcode )) end print("
NamePriceBarcode
%s%.02f€%s
") end) end 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 if path == "/" then if query.products then return r_products() elseif query.log then return r_log() elseif query.create_user then return r_create_user() else return r_index() end else 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 end