From 63c2e2fc3ea299aee31b66d2bdd6ecd871df223a Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 28 Oct 2026 22:44:41 +0100 Subject: v0.3.0: add server core, bcrypt migration and plays audit --- src/server/database.rs | 123 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 114 insertions(+), 9 deletions(-) (limited to 'src/server/database.rs') diff --git a/src/server/database.rs b/src/server/database.rs index c73aec5..f6c8872 100644 --- a/src/server/database.rs +++ b/src/server/database.rs @@ -1,4 +1,4 @@ -use rusqlite::Connection; +use rusqlite::{params, Connection}; pub fn init_db(conn: &Connection) { conn.execute( @@ -17,6 +17,21 @@ pub fn init_db(conn: &Connection) { "ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", [], ); + conn.execute( + "CREATE TABLE IF NOT EXISTS plays ( + session_id TEXT PRIMARY KEY, + username TEXT NOT NULL, + game_type TEXT NOT NULL, + bet_amount INTEGER NOT NULL, + winnings INTEGER NOT NULL, + reels TEXT NOT NULL DEFAULT '', + win_type TEXT NOT NULL DEFAULT '', + balance_after INTEGER NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )", + [], + ) + .unwrap(); } pub fn add_user( @@ -24,17 +39,28 @@ pub fn add_user( username: &str, hashed_password: &str, ) -> Result<(), Box> { - loop { + if get_hashed_password(conn, username).is_some() { + return Err("user already exists".into()); + } + + for _ in 0..10 { let account_id: u32 = rand::random::(); match conn.execute( - "INSERT INTO users (username, hashed_password, account_id, balance) VALUES (?1, ?2, ?3, ?4)", - [username, hashed_password, &account_id.to_string(), "1000"], + "INSERT INTO users (username, hashed_password, account_id, balance) + VALUES (?1, ?2, ?3, ?4)", + params![username, hashed_password, account_id as i64, 1000i64], ) { - Ok(_) => break, - Err(_) => continue, // Retry if account_id conflict + Ok(_) => return Ok(()), + Err(rusqlite::Error::SqliteFailure(error, _)) + if error.code == rusqlite::ErrorCode::ConstraintViolation => + { + continue; + } + Err(error) => return Err(Box::new(error)), } } - Ok(()) + + Err("could not allocate a unique account id".into()) } pub fn get_hashed_password(conn: &Connection, username: &str) -> Option { @@ -49,13 +75,25 @@ pub fn get_hashed_password(conn: &Connection, username: &str) -> Option } } -pub fn get_account_id(conn: &Connection, username: &str) -> Option { +pub fn set_hashed_password( + conn: &Connection, + username: &str, + hashed_password: &str, +) -> rusqlite::Result<()> { + conn.execute( + "UPDATE users SET hashed_password = ?1 WHERE username = ?2", + params![hashed_password, username], + )?; + Ok(()) +} + +pub fn get_account_id(conn: &Connection, username: &str) -> Option { let mut stmt = conn .prepare("SELECT account_id FROM users WHERE username = ?1") .unwrap(); let mut rows = stmt.query([username]).unwrap(); if let Ok(Some(row)) = rows.next() { - Some(row.get::<_, u32>(0).ok()?) + Some(row.get::<_, i64>(0).ok()?) } else { None } @@ -72,3 +110,70 @@ pub fn get_balance(conn: &Connection, username: &str) -> Option { None } } + +/// Settle one spin and record its audit row in a single transaction. +/// +/// Returns the balance after the spin. All SQL is parameterized: usernames +/// are data, never part of the statement text. +#[allow(clippy::too_many_arguments)] +pub fn apply_spin( + conn: &mut Connection, + session_id: &str, + username: &str, + game_type: &str, + bet_amount: i64, + winnings: i64, + reels: &str, + win_type: &str, +) -> rusqlite::Result { + let tx = conn.transaction()?; + + let deducted = tx.execute( + "UPDATE users SET balance = balance - ?1 + WHERE username = ?2 AND balance >= ?1", + params![bet_amount, username], + )?; + if deducted != 1 { + return Err(rusqlite::Error::QueryReturnedNoRows); + } + + tx.execute( + "UPDATE users SET balance = balance + ?1 WHERE username = ?2", + params![winnings, username], + )?; + + let balance_after: i64 = tx.query_row( + "SELECT balance FROM users WHERE username = ?1", + [username], + |row| row.get(0), + )?; + + tx.execute( + "INSERT INTO plays + (session_id, username, game_type, bet_amount, winnings, reels, + win_type, balance_after) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + session_id, + username, + game_type, + bet_amount, + winnings, + reels, + win_type, + balance_after + ], + )?; + + tx.commit()?; + Ok(balance_after) +} + +pub fn count_plays(conn: &Connection, username: &str) -> i64 { + conn.query_row( + "SELECT COUNT(*) FROM plays WHERE username = ?1", + [username], + |row| row.get(0), + ) + .unwrap_or(0) +} -- cgit v1.2.3