account rework for credits

This commit is contained in:
ntr 2019-06-27 16:01:56 +10:00
parent 4c3c81ade1
commit e6486dc361
5 changed files with 141 additions and 106 deletions

View File

@ -3,6 +3,7 @@ use bcrypt::{hash, verify};
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric; use rand::distributions::Alphanumeric;
use std::iter; use std::iter;
use std::convert::TryFrom;
use serde_cbor::{from_slice}; use serde_cbor::{from_slice};
use postgres::transaction::Transaction; use postgres::transaction::Transaction;
@ -11,7 +12,7 @@ use construct::{Construct, construct_recover};
use instance::{Instance, instance_delete}; use instance::{Instance, instance_delete};
use failure::Error; use failure::Error;
use failure::err_msg; use failure::{err_msg, format_err};
static PASSWORD_MIN_LEN: usize = 11; static PASSWORD_MIN_LEN: usize = 11;
@ -19,6 +20,121 @@ static PASSWORD_MIN_LEN: usize = 11;
pub struct Account { pub struct Account {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,
pub credits: u32,
}
impl Account {
pub fn select(tx: &mut Transaction, id: Uuid) -> Result<Account, Error> {
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<Account, Error> {
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<Account, Error> {
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<String, Error> {
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)] #[derive(Debug,Clone,Serialize,Deserialize)]
@ -29,29 +145,6 @@ struct AccountEntry {
token: String, token: String,
} }
pub fn account_from_token(token: String, tx: &mut Transaction) -> Result<Account, Error> {
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<String, Error> { pub fn account_create(name: &String, password: &String, code: &String, tx: &mut Transaction) -> Result<String, Error> {
if password.len() < PASSWORD_MIN_LEN { if password.len() < PASSWORD_MIN_LEN {
@ -95,70 +188,6 @@ pub fn account_create(name: &String, password: &String, code: &String, tx: &mut
Ok(token) Ok(token)
} }
pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> Result<String, Error> {
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<String, Error> {
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<Vec<Construct>, Error> { pub fn account_constructs(tx: &mut Transaction, account: &Account) -> Result<Vec<Construct>, Error> {
let query = " let query = "
SELECT data SELECT data

View File

@ -31,9 +31,10 @@ mod game;
mod instance; mod instance;
mod item; mod item;
mod mob; mod mob;
mod payments; mod mtx;
mod names; mod names;
mod net; mod net;
mod payments;
mod player; mod player;
mod pubsub; mod pubsub;
mod rpc; mod rpc;

View File

@ -16,7 +16,7 @@ use rpc::{RpcErrorResponse, AccountLoginParams, AccountCreateParams};
use warden::{warden}; use warden::{warden};
use pubsub::{pg_listen}; use pubsub::{pg_listen};
use ws::{connect}; 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}; use payments::{post_stripe_event};
pub type Db = PooledConnection<PostgresConnectionManager>; pub type Db = PooledConnection<PostgresConnectionManager>;
@ -82,8 +82,9 @@ fn login(state: web::Data<State>, params: web::Json::<AccountLoginParams>) -> Re
let db = state.pool.get().or(Err(MnmlHttpError::ServerError))?; let db = state.pool.get().or(Err(MnmlHttpError::ServerError))?;
let mut tx = db.transaction().or(Err(MnmlHttpError::ServerError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::ServerError))?;
match account_login(&params.name, &params.password, &mut tx) { match Account::login(&mut tx, &params.name, &params.password) {
Ok(token) => { Ok(a) => {
let token = a.new_token(&mut tx).or(Err(MnmlHttpError::ServerError))?;
tx.commit().or(Err(MnmlHttpError::ServerError))?; tx.commit().or(Err(MnmlHttpError::ServerError))?;
Ok(login_res(token)) Ok(login_res(token))
}, },
@ -99,9 +100,9 @@ fn logout(r: HttpRequest, state: web::Data<State>) -> Result<HttpResponse, MnmlH
Some(t) => { Some(t) => {
let db = state.pool.get().or(Err(MnmlHttpError::ServerError))?; let db = state.pool.get().or(Err(MnmlHttpError::ServerError))?;
let mut tx = db.transaction().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) => { 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))?; tx.commit().or(Err(MnmlHttpError::ServerError))?;
return Ok(logout_res()); return Ok(logout_res());
}, },

View File

@ -8,6 +8,11 @@ use failure::err_msg;
use stripe::{Event, EventObject, CheckoutSession}; use stripe::{Event, EventObject, CheckoutSession};
use net::{State, PgPool, MnmlHttpError}; 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 // Because the client_reference_id (account.id) is only included
// in the stripe CheckoutSession object // in the stripe CheckoutSession object
@ -24,7 +29,7 @@ enum StripeData {
} }
impl StripeData { impl StripeData {
fn persist(&self, tx: &mut Transaction) -> Result<&StripeData, Error> { fn insert(&self, tx: &mut Transaction) -> Result<&StripeData, Error> {
match self { match self {
StripeData::Customer { account, customer, checkout } => { StripeData::Customer { account, customer, checkout } => {
tx.execute(" 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 { match self {
StripeData::Subscription { subscription, account, customer, checkout } => { StripeData::Subscription { subscription: _, account, customer: _, checkout: _ } => {
// go get it let account = Account::select(tx, *account)?;
// set account sub to active and end date
Ok(self) Ok(self)
}, },
StripeData::Purchase { account, customer, amount, checkout } => { StripeData::Purchase { account: _, customer: _, amount, checkout: _ } => {
// create currency mtx and store amount.checked_div(CREDITS_COST_CENTS).expect("credits cost 0");
Ok(self) Ok(self)
}, },
_ => Ok(self) _ => Ok(self),
} }
} }
@ -120,8 +124,8 @@ fn process_stripe(event: Event, pool: &PgPool) -> Result<Vec<StripeData>, Error>
let mut tx = connection.transaction()?; let mut tx = connection.transaction()?;
for item in data.iter() { for item in data.iter() {
item.side_effects(&mut tx)?; item.insert(&mut tx)?;
item.persist(&mut tx)?; item.add_credits(&mut tx)?;
} }
tx.commit()?; tx.commit()?;

View File

@ -4,7 +4,7 @@ use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
use actix_web_actors::ws; use actix_web_actors::ws;
use actix::prelude::*; use actix::prelude::*;
use account::{Account, account_from_token}; use account::{Account};
use serde_cbor::{to_vec}; use serde_cbor::{to_vec};
use net::{PgPool, State, MnmlHttpError}; use net::{PgPool, State, MnmlHttpError};
@ -119,7 +119,7 @@ pub fn connect(r: HttpRequest, state: web::Data<State>, stream: web::Payload) ->
Some(t) => { Some(t) => {
let db = state.pool.get().or(Err(MnmlHttpError::ServerError))?; let db = state.pool.get().or(Err(MnmlHttpError::ServerError))?;
let mut tx = db.transaction().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), Ok(a) => Some(a),
Err(_) => None, Err(_) => None,
} }