account rework for credits
This commit is contained in:
parent
4c3c81ade1
commit
e6486dc361
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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(¶ms.name, ¶ms.password, &mut tx) {
|
match Account::login(&mut tx, ¶ms.name, ¶ms.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());
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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()?;
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user