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::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<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)]
@ -29,29 +145,6 @@ struct AccountEntry {
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> {
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<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> {
let query = "
SELECT data

View File

@ -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;

View File

@ -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<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 mut tx = db.transaction().or(Err(MnmlHttpError::ServerError))?;
match account_login(&params.name, &params.password, &mut tx) {
Ok(token) => {
match Account::login(&mut tx, &params.name, &params.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<State>) -> Result<HttpResponse, MnmlH
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) => {
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());
},

View File

@ -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<Vec<StripeData>, 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()?;

View File

@ -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<State>, 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,
}