use uuid::Uuid; 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; use http::MnmlHttpError; use names::{name as generate_name}; use construct::{Construct, ConstructSkeleton, construct_spawn}; use instance::{Instance, instance_delete}; use mtx::{Mtx, FREE_MTX}; use pg::Db; use img; use failure::Error; use failure::{err_msg, format_err}; static PASSWORD_MIN_LEN: usize = 11; #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Account { pub id: Uuid, pub img: Uuid, pub name: String, pub balance: u32, pub subscribed: bool, } impl<'a> TryFrom> for Account { type Error = Error; fn try_from(row: postgres::rows::Row) -> Result { let id: Uuid = row.get("id"); let img: Uuid = row.get("img"); let db_balance: i64 = row.get("balance"); let balance = u32::try_from(db_balance) .or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?; let subscribed: bool = row.get("subscribed"); let name: String = row.get("name"); Ok(Account { id, name, balance, subscribed, img }) } } pub fn select(db: &Db, id: Uuid) -> Result { let query = " SELECT id, name, balance, subscribed, img FROM accounts WHERE id = $1; "; let result = db .query(query, &[&id])?; let row = result.iter().next() .ok_or(format_err!("account not found {:?}", id))?; Account::try_from(row) } pub fn select_name(db: &Db, name: &String) -> Result { let query = " SELECT id, name, balance, subscribed, img FROM accounts WHERE name = $1; "; let result = db .query(query, &[&name])?; let row = result.iter().next() .ok_or(format_err!("account not found name={:?}", name))?; Account::try_from(row) } pub fn from_token(db: &Db, token: &String) -> Result { let query = " SELECT id, name, balance, subscribed, img FROM accounts WHERE token = $1 AND token_expiry > now(); "; let result = db .query(query, &[token])?; let row = result.iter().next() .ok_or(err_msg("invalid token"))?; Account::try_from(row) } pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result { let query = " SELECT id, password, name, balance, subscribed, img 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(MnmlHttpError::AccountNotFound); }, }; let hash: String = row.get(1); if !verify(password, &hash)? { return Err(MnmlHttpError::PasswordNotMatch); } let account = Account::try_from(row) .or(Err(MnmlHttpError::ServerError))?; Ok(account) } pub fn new_token(tx: &mut Transaction, id: Uuid) -> 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, &id])?; result.iter().next() .ok_or(MnmlHttpError::Unauthorized)?; Ok(token) } pub fn new_img(tx: &mut Transaction, id: Uuid) -> Result { let query = " UPDATE accounts SET img = $1, updated_at = now() WHERE id = $2 RETURNING id, password, name, balance, subscribed, img "; let result = tx .query(query, &[&Uuid::new_v4(), &id])?; let row = result.iter().next() .ok_or(format_err!("account not updated {:?}", id))?; Account::try_from(row) } pub fn set_password(tx: &mut Transaction, id: Uuid, current: &String, password: &String) -> Result { if password.len() < PASSWORD_MIN_LEN { return Err(MnmlHttpError::PasswordUnacceptable); } let query = " SELECT id, password FROM accounts WHERE id = $1 "; let result = tx .query(query, &[&id])?; 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(MnmlHttpError::AccountNotFound); }, }; let id: Uuid = row.get(0); let db_pw: String = row.get(1); // return bad request to prevent being logged out if !verify(current, &db_pw)? { return Err(MnmlHttpError::BadRequest); } let rounds = 8; let password = hash(&password, rounds)?; let query = " UPDATE accounts SET password = $1, updated_at = now() WHERE id = $2 RETURNING id, name; "; let result = tx .query(query, &[&password, &id])?; let row = match result.iter().next() { Some(row) => row, None => return Err(MnmlHttpError::DbError), }; let name: String = row.get(1); info!("password updated name={:?} id={:?}", name, id); new_token(tx, id) } pub fn credit(tx: &mut Transaction, id: Uuid, credits: i64) -> Result { let query = " UPDATE accounts SET balance = balance + $1 WHERE id = $2 RETURNING balance, name; "; let result = tx .query(query, &[&credits, &id])?; let row = result.iter().next() .ok_or(format_err!("account not updated {:?}", id))?; let db_balance: i64 = row.get(0); let balance = u32::try_from(db_balance) .or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?; let name: String = row.get(1); info!("account credited name={:?} credited={:?} balance={:?}", name, credits, balance); Ok(name) } pub fn debit(tx: &mut Transaction, id: Uuid, debit: i64) -> Result { let query = " UPDATE accounts SET balance = balance - $1 WHERE id = $2 RETURNING id, password, name, balance, subscribed, img "; let result = tx .query(query, &[&debit, &id]) .or(Err(err_msg("insufficient balance")))?; let row = result.iter().next() .ok_or(format_err!("account not found {:?}", id))?; let account = Account::try_from(row)?; info!("account debited name={:?} debited={:?} balance={:?}", account.name, debit, account.balance); Ok(account) } pub fn set_subscribed(tx: &mut Transaction, id: Uuid, subscribed: bool) -> Result { let query = " UPDATE accounts SET subscribed = $1 WHERE id = $2 RETURNING name; "; let result = tx .query(query, &[&subscribed, &id])?; let row = result.iter().next() .ok_or(format_err!("account not updated {:?}", id))?; let name: String = row.get(0); info!("account subscription status updated name={:?} subscribed={:?}", name, subscribed); Ok(name) } pub fn create(name: &String, password: &String, tx: &mut Transaction) -> Result { if password.len() < PASSWORD_MIN_LEN { return Err(MnmlHttpError::PasswordUnacceptable); } if name.len() == 0 { return Err(MnmlHttpError::AccountNameNotProvided); } let id = Uuid::new_v4(); let img = Uuid::new_v4(); let rounds = 8; let password = hash(&password, rounds)?; let mut rng = thread_rng(); let token: String = iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) .take(64) .collect(); let query = " INSERT INTO accounts (id, name, password, token, token_expiry, img) VALUES ($1, $2, $3, $4, now() + interval '1 week', $5) RETURNING id, name; "; let result = tx .query(query, &[&id, &name, &password, &token, &img])?; match result.iter().next() { Some(row) => row, None => return Err(MnmlHttpError::DbError), }; // 3 constructs for a team and 1 to swap for i in 0..4 { construct_spawn(tx, id, generate_name(), i < 3)?; } for mtx in FREE_MTX.iter() { Mtx::new(*mtx, id) .insert(tx)?; } // img::shapes_write(img)?; info!("registration account={:?}", name); Ok(token) } pub fn constructs(tx: &mut Transaction, account: &Account) -> Result, Error> { let query = " SELECT data FROM constructs WHERE account = $1; "; let result = tx .query(query, &[&account.id])?; let mut constructs = result.iter() .filter_map(|row| { let construct_bytes: Vec = row.get(0); match from_slice::(&construct_bytes) { Ok(s) => Some(s), Err(e) => { warn!("{:?}", e); None }, } }) .map(|mut sk| { sk.account = account.id; sk }) .map(|sk| Construct::from_skeleton(&sk)) .collect::>(); constructs.sort_by_key(|c| c.id); return Ok(constructs); } pub fn team(tx: &mut Transaction, account: &Account) -> Result, Error> { let query = " SELECT data FROM constructs WHERE account = $1 AND team = true; "; let result = tx .query(query, &[&account.id])?; let mut constructs = result.iter() .filter_map(|row| { let construct_bytes: Vec = row.get(0); match from_slice::(&construct_bytes) { Ok(s) => Some(s), Err(e) => { warn!("{:?}", e); None }, } }) .map(|mut sk| { sk.account = account.id; sk }) .map(|sk| Construct::from_skeleton(&sk)) .collect::>(); if constructs.len() != 3 { return Err(format_err!("team not size 3 account={:?}", account)); } constructs.sort_by_key(|c| c.id); return Ok(constructs); } // there is a trigger constraint on the table that enforces // exactly 3 constructs in a team pub fn set_team(tx: &mut Transaction, account: &Account, ids: Vec) -> Result, Error> { let query = " UPDATE constructs SET team = CASE WHEN id = ANY($2) THEN true ELSE false END WHERE account = $1; "; let _updated = tx .execute(query, &[&account.id, &ids])?; team(tx, account) } pub fn account_instances(tx: &mut Transaction, account: &Account) -> Result, Error> { let query = " SELECT data, id FROM instances WHERE finished = false AND id IN ( SELECT instance FROM players WHERE account = $1 ); "; let result = tx .query(query, &[&account.id])?; let mut list = vec![]; for row in result.into_iter() { let bytes: Vec = row.get(0); let id = row.get(1); match from_slice::(&bytes) { Ok(i) => list.push(i), Err(_e) => { instance_delete(tx, id)?; } }; } list.sort_unstable_by_key(|c| c.name.clone()); return Ok(list); } // all accounts have an image id but the img // doesn't necessarily exist until they subscribe pub fn img_check(account: &Account) -> Result { match account.subscribed { true => match img::exists(account.img) { true => Ok(account.img), false => img::shapes_write(account.img) }, false => Ok(account.img), } }