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 construct::{Construct, construct_recover}; use instance::{Instance, instance_delete}; 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 name: String, pub credits: u32, pub subscribed: bool, } impl Account { pub fn select(tx: &mut Transaction, id: Uuid) -> Result { let query = " SELECT id, name, credits, subscribed FROM accounts WHERE id = $1; "; let result = tx .query(query, &[&id])?; let row = result.iter().next() .ok_or(format_err!("account not found {:?}", id))?; let db_credits: i64 = row.get(2); let credits = u32::try_from(db_credits) .or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?; let subscribed: bool = row.get(3); Ok(Account { id, name: row.get(1), credits, subscribed }) } pub fn from_token(tx: &mut Transaction, token: String) -> Result { let query = " SELECT id, name, subscribed, 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 name: String = row.get(1); let subscribed: bool = row.get(2); let db_credits: i64 = row.get(3); let credits = u32::try_from(db_credits) .or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?; Ok(Account { id, name, credits, subscribed }) } pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result { let query = " SELECT id, password, name, credits, subscribed 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: i64 = row.get(3); let subscribed: bool = row.get(4); 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, subscribed }) } 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])?; let row = result.iter().next() .ok_or(format_err!("account not updated {:?}", id))?; let name: String = row.get(1); info!("login account={:?}", name); Ok(token) } pub fn add_credits(tx: &mut Transaction, id: Uuid, credits: i64) -> Result { let query = " UPDATE accounts SET credits = credits + $1 WHERE id = $2 RETURNING credits, name; "; let result = tx .query(query, &[&credits, &id])?; let row = result.iter().next() .ok_or(format_err!("account not updated {:?}", id))?; println!("{:?}", row); let db_credits: i64 = row.get(0); let total = u32::try_from(db_credits) .or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?; let name: String = row.get(1); info!("account credited name={:?} credited={:?} total={:?}", name, credits, total); Ok(name) } 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 account_create(name: &String, password: &String, code: &String, tx: &mut Transaction) -> Result { if password.len() < PASSWORD_MIN_LEN { return Err(err_msg("password must be at least 12 characters")); } if code.to_lowercase() != "grep842" { return Err(err_msg("https://discord.gg/YJJgurM")); } if name.len() == 0 { return Err(err_msg("account name not supplied")); } let id = 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) VALUES ($1, $2, $3, $4, now() + interval '1 week') RETURNING id, name; "; let result = tx .query(query, &[&id, &name, &password, &token])?; match result.iter().next() { Some(row) => row, None => return Err(err_msg("account not created")), }; info!("registration account={:?}", name); Ok(token) } pub fn account_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 constructs: Result, _> = result.iter() .map(|row| { let construct_bytes: Vec = row.get(0); match from_slice::(&construct_bytes) { Ok(c) => Ok(c), Err(_e) => construct_recover(construct_bytes, tx), } }) .collect(); // catch any errors if constructs.is_err() { warn!("{:?}", constructs); return Err(err_msg("could not deserialise a construct")); } let mut constructs = constructs.unwrap(); constructs.sort_by_key(|c| c.id); return Ok(constructs); } 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); }