2023-08-29 00:42:47 +00:00
|
|
|
--[[
|
|
|
|
bank.lua is the central bank server code, which runs 24/7 on a single computer
|
|
|
|
that is globally available via wireless connectivity (ender modem). It keeps a
|
|
|
|
persistent record of all accounts, and all transactions for accounts.
|
|
|
|
|
|
|
|
This program essentially serves as a simplified REST API for ingame clients,
|
|
|
|
like pocket computers and ATMs, for managing funds.
|
|
|
|
]]--
|
|
|
|
|
|
|
|
local USERS_DIR = "users"
|
|
|
|
local USER_DATA_FILE = "data.json"
|
|
|
|
local ACCOUNTS_FILE = "accounts.json"
|
|
|
|
local TRANSACTIONS_FILE = "transactions.json"
|
|
|
|
|
2023-08-29 13:07:05 +00:00
|
|
|
local HOST = "central-bank"
|
|
|
|
|
2023-08-29 13:21:24 +00:00
|
|
|
local RUNNING = true
|
|
|
|
|
2023-08-29 13:07:05 +00:00
|
|
|
local g = require("simple-graphics")
|
|
|
|
local W, H = term.getSize()
|
|
|
|
g.clear(term, colors.black)
|
2023-08-29 13:08:43 +00:00
|
|
|
g.drawTextCenter(term, W/2, 1, "BANK Server @ " .. HOST, colors.lime, colors.black)
|
2023-08-29 13:07:05 +00:00
|
|
|
g.drawXLine(term, 1, W, 2, colors.black, colors.gray, "-")
|
2023-08-29 13:21:24 +00:00
|
|
|
g.drawText(term, W-3, 1, "Quit", colors.white, colors.red)
|
2023-08-29 13:07:05 +00:00
|
|
|
|
|
|
|
local console = g.createConsole(W, H-2, colors.white, colors.black, "DOWN")
|
|
|
|
|
|
|
|
local function log(msg)
|
|
|
|
g.appendAndDrawConsole(term, console, textutils.formatTime(os.time()) .. ": " .. msg, 1, 3)
|
|
|
|
end
|
|
|
|
|
2023-08-29 00:42:47 +00:00
|
|
|
-- Basic account functions:
|
|
|
|
|
|
|
|
local function validateUsername(name)
|
|
|
|
local i, j = string.find(name, "%a%a%a+")
|
|
|
|
return (
|
|
|
|
i == 1 and
|
|
|
|
j == #name and
|
|
|
|
j <= 12
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function userDir(name)
|
|
|
|
return fs.combine(USERS_DIR, name)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function userDataFile(name)
|
|
|
|
return fs.combine(USERS_DIR, name, USER_DATA_FILE)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function userAccountsFile(name)
|
|
|
|
return fs.combine(USERS_DIR, name, ACCOUNTS_FILE)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function userExists(name)
|
|
|
|
return validateUsername(name) and fs.exists(userDir(name))
|
|
|
|
end
|
|
|
|
|
|
|
|
local function validatePassword(password)
|
|
|
|
return #password >= 8
|
|
|
|
end
|
|
|
|
|
|
|
|
local function randomAccountId()
|
|
|
|
local id = ""
|
|
|
|
for i = 1, 16 do
|
|
|
|
id = id .. tostring(math.random(1, 9))
|
|
|
|
if i % 4 == 0 and i < 16 then
|
|
|
|
id = id .. "-"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return id
|
|
|
|
end
|
|
|
|
|
|
|
|
local function getAccounts(name)
|
|
|
|
local f = io.open(userAccountsFile(name), "r")
|
|
|
|
local accounts = textutils.unserializeJSON(f:read("*a"))
|
|
|
|
f:close()
|
|
|
|
return accounts
|
|
|
|
end
|
|
|
|
|
|
|
|
local function saveAccounts(name, accounts)
|
|
|
|
local f = io.open(userAccountsFile(name), "w")
|
|
|
|
f:write(textutils.serializeJSON(accounts))
|
|
|
|
f:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
local function createUser(name, password)
|
|
|
|
if not validateUsername(name) then return false, "Invalid username" end
|
|
|
|
if not validatePassword(password) then return false, "Invalid password" end
|
|
|
|
if userExists(name) then return false, "Username taken" end
|
|
|
|
local userData = {
|
2023-08-29 12:03:06 +00:00
|
|
|
password = password,
|
|
|
|
createdAt = os.epoch("utc")
|
2023-08-29 00:42:47 +00:00
|
|
|
}
|
|
|
|
fs.makeDir(userDir(name))
|
|
|
|
local dataFile = io.open(userDataFile(name), "w")
|
|
|
|
dataFile:write(textutils.serializeJSON(userData))
|
|
|
|
dataFile:close()
|
|
|
|
-- Add an initial account.
|
|
|
|
local initialAccounts = {
|
|
|
|
{
|
|
|
|
id = randomAccountId(),
|
|
|
|
name = "Checking",
|
|
|
|
balance = 0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id = randomAccountId(),
|
|
|
|
name = "Savings",
|
|
|
|
balance = 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
saveAccounts(name, initialAccounts)
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
local function deleteUser(name)
|
|
|
|
if not userExists(name) then return false end
|
|
|
|
fs.delete(userDir(name))
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
local function renameUser(oldName, newName)
|
|
|
|
if not userExists(oldName) then return false, "User doesn't exist" end
|
|
|
|
if userExists(newName) then return false, "New username is taken" end
|
|
|
|
fs.move(userDir(oldName), userDir(newName))
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
2023-08-29 13:21:24 +00:00
|
|
|
-- Event handling
|
|
|
|
local function handleNetworkEvents()
|
|
|
|
log("Initializing Rednet hosting...")
|
|
|
|
rednet.open("top")
|
|
|
|
rednet.host("BANK", HOST)
|
|
|
|
log("Opened Rednet and hosted BANK at host \"" .. HOST .. "\".")
|
|
|
|
log("Now receiving requests.")
|
|
|
|
while RUNNING do
|
|
|
|
local remoteId, msg = rednet.receive("BANK", 3)
|
|
|
|
if remoteId ~= nil then
|
|
|
|
log("Received rednet message from computer ID " .. remoteId)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rednet.unhost("BANK")
|
|
|
|
rednet.close()
|
|
|
|
end
|
|
|
|
|
|
|
|
local function handleGuiEvents()
|
|
|
|
while RUNNING do
|
|
|
|
local event, button, x, y = os.pullEvent("mouse_click")
|
|
|
|
if button == 1 and y == 1 and x > W - 4 then
|
|
|
|
RUNNING = false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function handleEvents()
|
|
|
|
parallel.waitForAll(
|
|
|
|
handleNetworkEvents,
|
|
|
|
handleGuiEvents
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2023-08-29 12:03:06 +00:00
|
|
|
local args = {...}
|
|
|
|
|
|
|
|
if args[1] == "-i" then
|
|
|
|
print("Reinstalling from GitHub.")
|
|
|
|
fs.delete("bank.lua")
|
|
|
|
shell.execute("wget", "https://raw.githubusercontent.com/andrewlalis/kp-bank/main/bank.lua")
|
|
|
|
shell.execute("bank.lua")
|
|
|
|
end
|
|
|
|
|
2023-08-29 13:21:24 +00:00
|
|
|
handleEvents()
|