From e6486dc3610533ac9e2e09b8a8e2b4824f18e673 Mon Sep 17 00:00:00 2001 From: ntr Date: Thu, 27 Jun 2019 16:01:56 +1000 Subject: [PATCH] account rework for credits --- server/src/account.rs | 205 +++++++++++++++++++++++------------------ server/src/main.rs | 3 +- server/src/net.rs | 11 ++- server/src/payments.rs | 24 +++-- server/src/ws.rs | 4 +- 5 files changed, 141 insertions(+), 106 deletions(-) diff --git a/server/src/account.rs b/server/src/account.rs index e0b4b88b..7d3f0e27 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -3,6 +3,7 @@ use bcrypt::{hash, verify}; use rand::{thread_rng, Rng}; use rand::distributions::Alphanumeric; use std::iter; +use std::convert::TryFrom; use serde_cbor::{from_slice}; use postgres::transaction::Transaction; @@ -11,7 +12,7 @@ use construct::{Construct, construct_recover}; use instance::{Instance, instance_delete}; use failure::Error; -use failure::err_msg; +use failure::{err_msg, format_err}; static PASSWORD_MIN_LEN: usize = 11; @@ -19,6 +20,121 @@ static PASSWORD_MIN_LEN: usize = 11; pub struct Account { pub id: Uuid, pub name: String, + pub credits: u32, +} + +impl Account { + pub fn select(tx: &mut Transaction, id: Uuid) -> Result { + let query = " + SELECT id, name, credits + FROM accounts + WHERE id = $1 + FOR UPDATE; + "; + + let result = tx + .query(query, &[&id])?; + + let row = result.iter().next() + .ok_or(format_err!("account not found {:?}", id))?; + + let db_credits: i32 = row.get(2); + + let credits = u32::try_from(db_credits) + .or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?; + + Ok(Account { id, name: row.get(1), credits }) + } + + pub fn from_token(tx: &mut Transaction, token: String) -> Result { + let query = " + SELECT id, name, credits + FROM accounts + WHERE token = $1 + AND token_expiry > now(); + "; + + let result = tx + .query(query, &[&token])?; + + let row = result.iter().next() + .ok_or(err_msg("invalid token"))?; + + let id: Uuid = row.get(0); + let db_credits: i32 = row.get(2); + + let credits = u32::try_from(db_credits) + .or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?; + + Ok(Account { id, name: row.get(1), credits }) + } + + pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result { + let query = " + SELECT id, password, name, credits + FROM accounts + WHERE name = $1 + "; + + let result = tx + .query(query, &[&name])?; + + let row = match result.iter().next() { + Some(row) => row, + None => { + let mut rng = thread_rng(); + let garbage: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(64) + .collect(); + + // verify garbage to prevent timing attacks + verify(garbage.clone(), &garbage).ok(); + return Err(err_msg("account not found")); + }, + }; + + let id: Uuid = row.get(0); + let hash: String = row.get(1); + let name: String = row.get(2); + let db_credits: i32 = row.get(3); + + if !verify(password, &hash)? { + return Err(err_msg("password does not match")); + } + + let credits = u32::try_from(db_credits) + .or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?; + + Ok(Account { id, name, credits }) + } + + pub fn new_token(&self, tx: &mut Transaction) -> Result { + let mut rng = thread_rng(); + let token: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(64) + .collect(); + + // update token + let query = " + UPDATE accounts + SET token = $1, updated_at = now(), token_expiry = now() + interval '1 week' + WHERE id = $2 + RETURNING id, name; + "; + + let result = tx + .query(query, &[&token, &self.id])?; + + let _row = result.iter().next() + .ok_or(format_err!("account not updated {:?}", self.id))?; + + info!("login account={:?}", self.name); + + Ok(token) + } + } #[derive(Debug,Clone,Serialize,Deserialize)] @@ -29,29 +145,6 @@ struct AccountEntry { token: String, } -pub fn account_from_token(token: String, tx: &mut Transaction) -> Result { - let query = " - SELECT id, name, token - FROM accounts - WHERE token = $1 - AND token_expiry > now(); - "; - - let result = tx - .query(query, &[&token])?; - - let returned = match result.iter().next() { - Some(row) => row, - None => return Err(err_msg("invalid token")), - }; - - let entry = Account { - id: returned.get(0), - name: returned.get(1), - }; - - return Ok(entry); -} pub fn account_create(name: &String, password: &String, code: &String, tx: &mut Transaction) -> Result { if password.len() < PASSWORD_MIN_LEN { @@ -95,70 +188,6 @@ pub fn account_create(name: &String, password: &String, code: &String, tx: &mut Ok(token) } -pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> Result { - let query = " - SELECT id, password, name - FROM accounts - WHERE name = $1 - "; - - let result = tx - .query(query, &[&name])?; - - let returned = match result.iter().next() { - Some(row) => row, - None => { - let mut rng = thread_rng(); - let garbage: String = iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) - .take(64) - .collect(); - - // verify garbage to prevent timing attacks - verify(garbage.clone(), &garbage).ok(); - return Err(err_msg("account not found")); - }, - }; - - let account = Account { - id: returned.get(0), - name: returned.get(2), - }; - - let hash: String = returned.get(1); - - if !verify(password, &hash)? { - return Err(err_msg("password does not match")); - } - - account_set_token(tx, &account) -} - -pub fn account_set_token(tx: &mut Transaction, account: &Account) -> Result { - let mut rng = thread_rng(); - let token: String = iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) - .take(64) - .collect(); - - // update token - let query = " - UPDATE accounts - SET token = $1, updated_at = now(), token_expiry = now() + interval '1 week' - WHERE id = $2 - RETURNING id; - "; - - let result = tx - .query(query, &[&token, &account.id])?; - - result.iter().next().ok_or(format_err!("user {:?} could not be updated", account.id))?; - - info!("login account={:?}", account.name); - - Ok(token) -} - pub fn account_constructs(tx: &mut Transaction, account: &Account) -> Result, Error> { let query = " SELECT data diff --git a/server/src/main.rs b/server/src/main.rs index ee55392a..312652f4 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -31,9 +31,10 @@ mod game; mod instance; mod item; mod mob; -mod payments; +mod mtx; mod names; mod net; +mod payments; mod player; mod pubsub; mod rpc; diff --git a/server/src/net.rs b/server/src/net.rs index 0a3964b9..66428570 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -16,7 +16,7 @@ use rpc::{RpcErrorResponse, AccountLoginParams, AccountCreateParams}; use warden::{warden}; use pubsub::{pg_listen}; use ws::{connect}; -use account::{account_login, account_create, account_from_token, account_set_token}; +use account::{Account, account_create}; use payments::{post_stripe_event}; pub type Db = PooledConnection; @@ -82,8 +82,9 @@ fn login(state: web::Data, params: web::Json::) -> Re let db = state.pool.get().or(Err(MnmlHttpError::ServerError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::ServerError))?; - match account_login(¶ms.name, ¶ms.password, &mut tx) { - Ok(token) => { + match Account::login(&mut tx, ¶ms.name, ¶ms.password) { + Ok(a) => { + let token = a.new_token(&mut tx).or(Err(MnmlHttpError::ServerError))?; tx.commit().or(Err(MnmlHttpError::ServerError))?; Ok(login_res(token)) }, @@ -99,9 +100,9 @@ fn logout(r: HttpRequest, state: web::Data) -> Result { let db = state.pool.get().or(Err(MnmlHttpError::ServerError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::ServerError))?; - match account_from_token(t.value().to_string(), &mut tx) { + match Account::from_token(&mut tx, t.value().to_string()) { Ok(a) => { - account_set_token(&mut tx, &a).or(Err(MnmlHttpError::Unauthorized))?; + a.new_token(&mut tx).or(Err(MnmlHttpError::Unauthorized))?; tx.commit().or(Err(MnmlHttpError::ServerError))?; return Ok(logout_res()); }, diff --git a/server/src/payments.rs b/server/src/payments.rs index 17a1c371..a4fede82 100644 --- a/server/src/payments.rs +++ b/server/src/payments.rs @@ -8,6 +8,11 @@ use failure::err_msg; use stripe::{Event, EventObject, CheckoutSession}; use net::{State, PgPool, MnmlHttpError}; +use account::{Account}; + +// we use i64 because it is converted to BIGINT for pg +const CREDITS_COST_CENTS: i64 = 20; +const CREDITS_SUB_BONUS: i64 = 25; // Because the client_reference_id (account.id) is only included // in the stripe CheckoutSession object @@ -24,7 +29,7 @@ enum StripeData { } impl StripeData { - fn persist(&self, tx: &mut Transaction) -> Result<&StripeData, Error> { + fn insert(&self, tx: &mut Transaction) -> Result<&StripeData, Error> { match self { StripeData::Customer { account, customer, checkout } => { tx.execute(" @@ -55,18 +60,17 @@ impl StripeData { } } - fn side_effects(&self, tx: &mut Transaction) -> Result<&StripeData, Error> { + fn add_credits(&self, tx: &mut Transaction) -> Result<&StripeData, Error> { match self { - StripeData::Subscription { subscription, account, customer, checkout } => { - // go get it - // set account sub to active and end date + StripeData::Subscription { subscription: _, account, customer: _, checkout: _ } => { + let account = Account::select(tx, *account)?; Ok(self) }, - StripeData::Purchase { account, customer, amount, checkout } => { - // create currency mtx and store + StripeData::Purchase { account: _, customer: _, amount, checkout: _ } => { + amount.checked_div(CREDITS_COST_CENTS).expect("credits cost 0"); Ok(self) }, - _ => Ok(self) + _ => Ok(self), } } @@ -120,8 +124,8 @@ fn process_stripe(event: Event, pool: &PgPool) -> Result, Error> let mut tx = connection.transaction()?; for item in data.iter() { - item.side_effects(&mut tx)?; - item.persist(&mut tx)?; + item.insert(&mut tx)?; + item.add_credits(&mut tx)?; } tx.commit()?; diff --git a/server/src/ws.rs b/server/src/ws.rs index a615b5b4..2bbef5d3 100644 --- a/server/src/ws.rs +++ b/server/src/ws.rs @@ -4,7 +4,7 @@ use actix_web::{web, HttpMessage, HttpRequest, HttpResponse}; use actix_web_actors::ws; use actix::prelude::*; -use account::{Account, account_from_token}; +use account::{Account}; use serde_cbor::{to_vec}; use net::{PgPool, State, MnmlHttpError}; @@ -119,7 +119,7 @@ pub fn connect(r: HttpRequest, state: web::Data, stream: web::Payload) -> Some(t) => { let db = state.pool.get().or(Err(MnmlHttpError::ServerError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::ServerError))?; - match account_from_token(t.value().to_string(), &mut tx) { + match Account::from_token(&mut tx, t.value().to_string()) { Ok(a) => Some(a), Err(_) => None, }