summaryrefslogtreecommitdiffstats
path: root/src/server/database.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/server/database.rs123
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 @@
1use rusqlite::Connection; 1use rusqlite::{params, Connection};
2 2
3pub fn init_db(conn: &Connection) { 3pub 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
22pub fn add_user( 37pub 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
40pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> { 66pub 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
52pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> { 78pub 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
90pub 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)]
119pub 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
172pub 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}