diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-28 22:44:41 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-28 22:44:41 +0100 |
| commit | 63c2e2fc3ea299aee31b66d2bdd6ecd871df223a (patch) | |
| tree | 9c85ab7a4de17125055257dc055fa3e53dc1e8cf /src/server/database.rs | |
| parent | v0.2.6: preserve buffered lines and add request read timeouts (diff) | |
| download | zagan-bet-63c2e2fc3ea299aee31b66d2bdd6ecd871df223a.tar.gz zagan-bet-63c2e2fc3ea299aee31b66d2bdd6ecd871df223a.zip | |
v0.3.0: add server core, bcrypt migration and plays audit
Diffstat (limited to '')
| -rw-r--r-- | src/server/database.rs | 123 |
1 files changed, 114 insertions, 9 deletions
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 @@ | |||
| 1 | use rusqlite::Connection; | 1 | use rusqlite::{params, Connection}; |
| 2 | 2 | ||
| 3 | pub fn init_db(conn: &Connection) { | 3 | pub fn init_db(conn: &Connection) { |
| 4 | conn.execute( | 4 | conn.execute( |
| @@ -17,6 +17,21 @@ pub fn init_db(conn: &Connection) { | |||
| 17 | "ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", | 17 | "ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", |
| 18 | [], | 18 | [], |
| 19 | ); | 19 | ); |
| 20 | conn.execute( | ||
| 21 | "CREATE TABLE IF NOT EXISTS plays ( | ||
| 22 | session_id TEXT PRIMARY KEY, | ||
| 23 | username TEXT NOT NULL, | ||
| 24 | game_type TEXT NOT NULL, | ||
| 25 | bet_amount INTEGER NOT NULL, | ||
| 26 | winnings INTEGER NOT NULL, | ||
| 27 | reels TEXT NOT NULL DEFAULT '', | ||
| 28 | win_type TEXT NOT NULL DEFAULT '', | ||
| 29 | balance_after INTEGER NOT NULL, | ||
| 30 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP | ||
| 31 | )", | ||
| 32 | [], | ||
| 33 | ) | ||
| 34 | .unwrap(); | ||
| 20 | } | 35 | } |
| 21 | 36 | ||
| 22 | pub fn add_user( | 37 | pub fn add_user( |
| @@ -24,17 +39,28 @@ pub fn add_user( | |||
| 24 | username: &str, | 39 | username: &str, |
| 25 | hashed_password: &str, | 40 | hashed_password: &str, |
| 26 | ) -> Result<(), Box<dyn std::error::Error>> { | 41 | ) -> Result<(), Box<dyn std::error::Error>> { |
| 27 | loop { | 42 | if get_hashed_password(conn, username).is_some() { |
| 43 | return Err("user already exists".into()); | ||
| 44 | } | ||
| 45 | |||
| 46 | for _ in 0..10 { | ||
| 28 | let account_id: u32 = rand::random::<u32>(); | 47 | let account_id: u32 = rand::random::<u32>(); |
| 29 | match conn.execute( | 48 | match conn.execute( |
| 30 | "INSERT INTO users (username, hashed_password, account_id, balance) VALUES (?1, ?2, ?3, ?4)", | 49 | "INSERT INTO users (username, hashed_password, account_id, balance) |
| 31 | [username, hashed_password, &account_id.to_string(), "1000"], | 50 | VALUES (?1, ?2, ?3, ?4)", |
| 51 | params![username, hashed_password, account_id as i64, 1000i64], | ||
| 32 | ) { | 52 | ) { |
| 33 | Ok(_) => break, | 53 | Ok(_) => return Ok(()), |
| 34 | Err(_) => continue, // Retry if account_id conflict | 54 | Err(rusqlite::Error::SqliteFailure(error, _)) |
| 55 | if error.code == rusqlite::ErrorCode::ConstraintViolation => | ||
| 56 | { | ||
| 57 | continue; | ||
| 58 | } | ||
| 59 | Err(error) => return Err(Box::new(error)), | ||
| 35 | } | 60 | } |
| 36 | } | 61 | } |
| 37 | Ok(()) | 62 | |
| 63 | Err("could not allocate a unique account id".into()) | ||
| 38 | } | 64 | } |
| 39 | 65 | ||
| 40 | pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> { | 66 | pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> { |
| @@ -49,13 +75,25 @@ pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> | |||
| 49 | } | 75 | } |
| 50 | } | 76 | } |
| 51 | 77 | ||
| 52 | pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> { | 78 | pub fn set_hashed_password( |
| 79 | conn: &Connection, | ||
| 80 | username: &str, | ||
| 81 | hashed_password: &str, | ||
| 82 | ) -> rusqlite::Result<()> { | ||
| 83 | conn.execute( | ||
| 84 | "UPDATE users SET hashed_password = ?1 WHERE username = ?2", | ||
| 85 | params![hashed_password, username], | ||
| 86 | )?; | ||
| 87 | Ok(()) | ||
| 88 | } | ||
| 89 | |||
| 90 | pub fn get_account_id(conn: &Connection, username: &str) -> Option<i64> { | ||
| 53 | let mut stmt = conn | 91 | let mut stmt = conn |
| 54 | .prepare("SELECT account_id FROM users WHERE username = ?1") | 92 | .prepare("SELECT account_id FROM users WHERE username = ?1") |
| 55 | .unwrap(); | 93 | .unwrap(); |
| 56 | let mut rows = stmt.query([username]).unwrap(); | 94 | let mut rows = stmt.query([username]).unwrap(); |
| 57 | if let Ok(Some(row)) = rows.next() { | 95 | if let Ok(Some(row)) = rows.next() { |
| 58 | Some(row.get::<_, u32>(0).ok()?) | 96 | Some(row.get::<_, i64>(0).ok()?) |
| 59 | } else { | 97 | } else { |
| 60 | None | 98 | None |
| 61 | } | 99 | } |
| @@ -72,3 +110,70 @@ pub fn get_balance(conn: &Connection, username: &str) -> Option<i64> { | |||
| 72 | None | 110 | None |
| 73 | } | 111 | } |
| 74 | } | 112 | } |
| 113 | |||
| 114 | /// Settle one spin and record its audit row in a single transaction. | ||
| 115 | /// | ||
| 116 | /// Returns the balance after the spin. All SQL is parameterized: usernames | ||
| 117 | /// are data, never part of the statement text. | ||
| 118 | #[allow(clippy::too_many_arguments)] | ||
| 119 | pub fn apply_spin( | ||
| 120 | conn: &mut Connection, | ||
| 121 | session_id: &str, | ||
| 122 | username: &str, | ||
| 123 | game_type: &str, | ||
| 124 | bet_amount: i64, | ||
| 125 | winnings: i64, | ||
| 126 | reels: &str, | ||
| 127 | win_type: &str, | ||
| 128 | ) -> rusqlite::Result<i64> { | ||
| 129 | let tx = conn.transaction()?; | ||
| 130 | |||
| 131 | let deducted = tx.execute( | ||
| 132 | "UPDATE users SET balance = balance - ?1 | ||
| 133 | WHERE username = ?2 AND balance >= ?1", | ||
| 134 | params![bet_amount, username], | ||
| 135 | )?; | ||
| 136 | if deducted != 1 { | ||
| 137 | return Err(rusqlite::Error::QueryReturnedNoRows); | ||
| 138 | } | ||
| 139 | |||
| 140 | tx.execute( | ||
| 141 | "UPDATE users SET balance = balance + ?1 WHERE username = ?2", | ||
| 142 | params![winnings, username], | ||
| 143 | )?; | ||
| 144 | |||
| 145 | let balance_after: i64 = tx.query_row( | ||
| 146 | "SELECT balance FROM users WHERE username = ?1", | ||
| 147 | [username], | ||
| 148 | |row| row.get(0), | ||
| 149 | )?; | ||
| 150 | |||
| 151 | tx.execute( | ||
| 152 | "INSERT INTO plays | ||
| 153 | (session_id, username, game_type, bet_amount, winnings, reels, | ||
| 154 | win_type, balance_after) | ||
| 155 | VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", | ||
| 156 | params![ | ||
| 157 | session_id, | ||
| 158 | username, | ||
| 159 | game_type, | ||
| 160 | bet_amount, | ||
| 161 | winnings, | ||
| 162 | reels, | ||
| 163 | win_type, | ||
| 164 | balance_after | ||
| 165 | ], | ||
| 166 | )?; | ||
| 167 | |||
| 168 | tx.commit()?; | ||
| 169 | Ok(balance_after) | ||
| 170 | } | ||
| 171 | |||
| 172 | pub fn count_plays(conn: &Connection, username: &str) -> i64 { | ||
| 173 | conn.query_row( | ||
| 174 | "SELECT COUNT(*) FROM plays WHERE username = ?1", | ||
| 175 | [username], | ||
| 176 | |row| row.get(0), | ||
| 177 | ) | ||
| 178 | .unwrap_or(0) | ||
| 179 | } | ||
