Added networking and basic handlers.

This commit is contained in:
Andrew Lalis 2023-08-29 11:26:33 -04:00
parent 85dbe20cd1
commit da47304eec
1 changed files with 184 additions and 27 deletions

211
bank.lua
View File

@ -10,7 +10,6 @@ like pocket computers and ATMs, for managing funds.
local USERS_DIR = "users" local USERS_DIR = "users"
local USER_DATA_FILE = "data.json" local USER_DATA_FILE = "data.json"
local ACCOUNTS_FILE = "accounts.json" local ACCOUNTS_FILE = "accounts.json"
local TRANSACTIONS_FILE = "transactions.json"
local HOST = "central-bank" local HOST = "central-bank"
@ -29,6 +28,23 @@ local function log(msg)
g.appendAndDrawConsole(term, console, textutils.formatTime(os.time()) .. ": " .. msg, 1, 3) g.appendAndDrawConsole(term, console, textutils.formatTime(os.time()) .. ": " .. msg, 1, 3)
end end
-- Helper functions
local function readJSON(filename)
local f = io.open(filename, "r")
if not f then error("Cannot open file " .. filename .. " to read JSON data.") end
local data = textutils.unserializeJSON(f:read("*a"))
f:close()
return data
end
local function writeJSON(filename, data)
local f = io.open(filename, "w")
if not f then error("Cannot open file " .. filename .. " to write JSON data.") end
f:write(textutils.serializeJSON(data))
f:close()
end
-- Basic account functions: -- Basic account functions:
local function validateUsername(name) local function validateUsername(name)
@ -52,6 +68,10 @@ local function userAccountsFile(name)
return fs.combine(USERS_DIR, name, ACCOUNTS_FILE) return fs.combine(USERS_DIR, name, ACCOUNTS_FILE)
end end
local function accountTransactionsFile(username, accountId)
return fs.combine(USERS_DIR, username, "tx_" .. accountId .. ".txt")
end
local function userExists(name) local function userExists(name)
return validateUsername(name) and fs.exists(userDir(name)) return validateUsername(name) and fs.exists(userDir(name))
end end
@ -63,7 +83,7 @@ end
local function randomAccountId() local function randomAccountId()
local id = "" local id = ""
for i = 1, 16 do for i = 1, 16 do
id = id .. tostring(math.random(1, 9)) id = id .. tostring(math.random(0, 9))
if i % 4 == 0 and i < 16 then if i % 4 == 0 and i < 16 then
id = id .. "-" id = id .. "-"
end end
@ -71,17 +91,69 @@ local function randomAccountId()
return id return id
end end
local function getUserData(name)
return readJSON(userDataFile(name))
end
local function getAccounts(name) local function getAccounts(name)
local f = io.open(userAccountsFile(name), "r") return readJSON(userAccountsFile(name))
local accounts = textutils.unserializeJSON(f:read("*a"))
f:close()
return accounts
end end
local function saveAccounts(name, accounts) local function saveAccounts(name, accounts)
local f = io.open(userAccountsFile(name), "w") writeJSON(userAccountsFile(name), accounts)
f:write(textutils.serializeJSON(accounts)) end
f:close()
local function createAccount(username, accountName)
local accounts = getAccounts(username)
for i, account in pairs(accounts) do
if account.name == accountName then
return false, "Duplicate account name"
end
end
local newAccount = {
id = randomAccountId(),
name = accountName,
balance = 0,
createdAt = os.epoch("utc")
}
table.insert(accounts, newAccount)
saveAccounts(username, accounts)
log("Created account " .. newAccount.id .. " for user " .. username)
return true, newAccount.id
end
local function deleteAccount(username, accountId)
local accounts = getAccounts(username)
local targetIndex = nil
for i, account in pairs(accounts) do
if account.id == accountId then
targetIndex = i
end
end
if targetIndex then
table.remove(accounts, targetIndex)
saveAccounts(username, accounts)
log("Deleted user " .. username .. " account " .. accountId)
return true
end
return false
end
local function renameAccount(username, accountId, newName)
local accounts = getAccounts(username)
local targetAccount = nil
for i, account in pairs(accounts) do
if account.id == accountId then
targetAccount = account
elseif account.name == newName then
return false, "Duplicate account name"
end
end
if not targetAccount then return false, "Account not found" end
targetAccount.name = newName
saveAccounts(accounts)
log("Renamed user " .. username .. " account " .. accountId .. " to " .. newName)
return true
end end
local function createUser(name, password) local function createUser(name, password)
@ -93,40 +165,115 @@ local function createUser(name, password)
createdAt = os.epoch("utc") createdAt = os.epoch("utc")
} }
fs.makeDir(userDir(name)) fs.makeDir(userDir(name))
local dataFile = io.open(userDataFile(name), "w") writeJSON(userDataFile(name), userData) -- Flush user data file.
dataFile:write(textutils.serializeJSON(userData)) saveAccounts(userAccountsFile(name), {}) -- Flush initial accounts file.
dataFile:close() createAccount(name, "Checking")
-- Add an initial account. createAccount(name, "Savings")
local initialAccounts = { log("Created new user: " .. name)
{
id = randomAccountId(),
name = "Checking",
balance = 0
},
{
id = randomAccountId(),
name = "Savings",
balance = 0
}
}
saveAccounts(name, initialAccounts)
return true return true
end end
local function deleteUser(name) local function deleteUser(name)
if not userExists(name) then return false end if not userExists(name) then return false end
fs.delete(userDir(name)) fs.delete(userDir(name))
log("Deleted user \"" .. name .. "\".")
return true return true
end end
local function renameUser(oldName, newName) local function renameUser(oldName, newName)
if not validateUsername(newName) then return false, "Invalid new username" end
if not userExists(oldName) then return false, "User doesn't exist" end if not userExists(oldName) then return false, "User doesn't exist" end
if userExists(newName) then return false, "New username is taken" end if userExists(newName) then return false, "New username is taken" end
fs.move(userDir(oldName), userDir(newName)) fs.move(userDir(oldName), userDir(newName))
log("Renamed user \"" .. oldName .. "\" to \"" .. newName .. "\".")
return true return true
end end
-- Event handling -- EVENT HANDLING
-----------------
-- Helper function to wrap another function in an authentication check.
local function authProtect(func)
return function (msg)
if (
not msg.auth or
not msg.auth.username or
not msg.auth.password or
not userExists(msg.auth.username) or
getUserData(msg.auth.username).password ~= msg.auth.password
) then
return {success = false, error = "Invalid credentials"}
end
return func(msg)
end
end
local function handleCreateUser(msg)
if not msg.data or not msg.data.username or not msg.data.password then
return {success = false, error = "Invalid request. Requires data.username and data.password."}
end
local success, errorMsg = createuser(msg.data.username, msg.data.password)
if not success then
return {success = false, error = errorMsg}
end
return {success = true}
end
local function handleDeleteUser(msg)
deleteUser(msg.auth.username)
return {success = true}
end
local function handleRenameUser(msg)
if not msg.data or not msg.data.newUsername then
return {success = false, error = "Invalid request. Requires data.newUsername."}
end
local success, errorMsg = renameUser(msg.auth.username, msg.data.newUsername)
if not success then
return {success = false, error = errorMsg}
end
return {success = true}
end
local function handleGetUserAccounts(msg)
return {success = true, data = getAccounts(msg.auth.username)}
end
local function handleDeleteUserAccount(msg)
if not msg.data or not msg.data.accountId then
return {success = false, error = "Invalid request. Requires data.accountId."}
end
deleteAccount(msg.auth.username, msg.data.accountId)
return {success = true}
end
local function handleRenameUserAccount(msg)
if not msg.data or not msg.data.accountId or not msg.data.newName then
return {success = false, error = "Invalid request. Requires data.accountId and data.newName."}
end
local success, errorMsg = renameAccount(msg.auth.username, msg.data.accountid, msg.data.newName)
return {success = success, error = errorMsg}
end
local function BANK_REQUESTS = {
["CREATE_USER"] = handleCreateUser,
["DELETE_USER"] = authProtect(handleDeleteUser)
["RENAME_USER"] = authProtect(handleRenameUser)
["GET_ACCOUNTS"] = authProtect(handleGetUserAccounts)
["DELETE_ACCOUNT"] = authProtect(handleDeleteUserAccount)
["RENAME_ACCOUNT"] = authProtect(handleRenameUserAccount)
}
local function handleBankMessage(remoteId, msg)
if msg == nil or msg.command == nil or type(msg.command) ~= "string" then
return {success = false, error = "Invalid BANK request. Message is nil or missing \"command\" string property."}
end
if BANK_REQUESTS[msg.command] then
return BANK_REQUESTS[msg.command](msg)
end
return {success = false, error = "Unknown command: \"" .. msg.command .. "\""}
end
local function handleNetworkEvents() local function handleNetworkEvents()
log("Initializing Rednet hosting...") log("Initializing Rednet hosting...")
rednet.open("top") rednet.open("top")
@ -137,6 +284,11 @@ local function handleNetworkEvents()
local remoteId, msg = rednet.receive("BANK", 3) local remoteId, msg = rednet.receive("BANK", 3)
if remoteId ~= nil then if remoteId ~= nil then
log("Received rednet message from computer ID " .. remoteId) log("Received rednet message from computer ID " .. remoteId)
local success, response = pcall(handleBankMessage, remoteId, msg)
if not success then
response = {success = false, error = "An error occurred: " .. response}
end
rednet.send(remoteId, response, "BANK")
end end
end end
rednet.unhost("BANK") rednet.unhost("BANK")
@ -147,9 +299,14 @@ local function handleGuiEvents()
while RUNNING do while RUNNING do
local event, button, x, y = os.pullEvent("mouse_click") local event, button, x, y = os.pullEvent("mouse_click")
if button == 1 and y == 1 and x > W - 4 then if button == 1 and y == 1 and x > W - 4 then
log("Quitting...")
RUNNING = false RUNNING = false
end end
end end
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
term.setCursorPos(1, 1)
end end
local function handleEvents() local function handleEvents()