From da47304eec472dcbe32b351dde75bee6f5576c6c Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Tue, 29 Aug 2023 11:26:33 -0400 Subject: [PATCH] Added networking and basic handlers. --- bank.lua | 211 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 184 insertions(+), 27 deletions(-) diff --git a/bank.lua b/bank.lua index 682dd9d..80eacbd 100644 --- a/bank.lua +++ b/bank.lua @@ -10,7 +10,6 @@ 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" local HOST = "central-bank" @@ -29,6 +28,23 @@ local function log(msg) g.appendAndDrawConsole(term, console, textutils.formatTime(os.time()) .. ": " .. msg, 1, 3) 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: local function validateUsername(name) @@ -52,6 +68,10 @@ local function userAccountsFile(name) return fs.combine(USERS_DIR, name, ACCOUNTS_FILE) end +local function accountTransactionsFile(username, accountId) + return fs.combine(USERS_DIR, username, "tx_" .. accountId .. ".txt") +end + local function userExists(name) return validateUsername(name) and fs.exists(userDir(name)) end @@ -63,7 +83,7 @@ end local function randomAccountId() local id = "" 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 id = id .. "-" end @@ -71,17 +91,69 @@ local function randomAccountId() return id end +local function getUserData(name) + return readJSON(userDataFile(name)) +end + local function getAccounts(name) - local f = io.open(userAccountsFile(name), "r") - local accounts = textutils.unserializeJSON(f:read("*a")) - f:close() - return accounts + return readJSON(userAccountsFile(name)) end local function saveAccounts(name, accounts) - local f = io.open(userAccountsFile(name), "w") - f:write(textutils.serializeJSON(accounts)) - f:close() + writeJSON(userAccountsFile(name), accounts) +end + +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 local function createUser(name, password) @@ -93,40 +165,115 @@ local function createUser(name, password) createdAt = os.epoch("utc") } 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) + writeJSON(userDataFile(name), userData) -- Flush user data file. + saveAccounts(userAccountsFile(name), {}) -- Flush initial accounts file. + createAccount(name, "Checking") + createAccount(name, "Savings") + log("Created new user: " .. name) return true end local function deleteUser(name) if not userExists(name) then return false end fs.delete(userDir(name)) + log("Deleted user \"" .. name .. "\".") return true end 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 userExists(newName) then return false, "New username is taken" end fs.move(userDir(oldName), userDir(newName)) + log("Renamed user \"" .. oldName .. "\" to \"" .. newName .. "\".") return true 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() log("Initializing Rednet hosting...") rednet.open("top") @@ -137,6 +284,11 @@ local function handleNetworkEvents() local remoteId, msg = rednet.receive("BANK", 3) if remoteId ~= nil then 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 rednet.unhost("BANK") @@ -147,9 +299,14 @@ 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 + log("Quitting...") RUNNING = false end end + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + term.clear() + term.setCursorPos(1, 1) end local function handleEvents()