mnml/server/src/account.rs
2019-11-13 21:35:52 +11:00

530 lines
13 KiB
Rust

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 instance;
use mtx::{Mtx, FREE_MTX};
use pg::Db;
use img;
use failure::Error;
use failure::{err_msg, format_err};
static PASSWORD_MIN_LEN: usize = 3;
static PASSWORD_ROUNDS: u32 = 10;
#[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<postgres::rows::Row<'a>> for Account {
type Error = Error;
fn try_from(row: postgres::rows::Row) -> Result<Self, Error> {
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<Account, Error> {
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 chat_wheel(_db: &Db, _id: Uuid) -> Result<Vec<String>, Error> {
return Ok(vec![
"gg".to_string(),
"glhf".to_string(),
"ez".to_string(),
"rekt".to_string(),
"wow".to_string(),
"wp".to_string(),
"ok".to_string(),
"...".to_string(),
])
}
pub fn select_name(db: &Db, name: &String) -> Result<Account, Error> {
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<Account, Error> {
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<Account, MnmlHttpError> {
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<String, MnmlHttpError> {
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<Account, Error> {
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<String, MnmlHttpError> {
if password.len() < PASSWORD_MIN_LEN || password.len() > 100 {
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 password = hash(&password, 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<String, Error> {
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<Account, Error> {
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<String, Error> {
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<String, MnmlHttpError> {
if password.len() < PASSWORD_MIN_LEN || password.len() > 100 {
return Err(MnmlHttpError::PasswordUnacceptable);
}
if name.len() == 0 {
return Err(MnmlHttpError::AccountNameNotProvided);
}
if name.len() > 20 {
return Err(MnmlHttpError::AccountNameUnacceptable);
}
let id = Uuid::new_v4();
let img = Uuid::new_v4();
let password = hash(&password, 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<Vec<Construct>, 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<u8> = row.get(0);
match from_slice::<ConstructSkeleton>(&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::<Vec<Construct>>();
constructs.sort_by_key(|c| c.id);
return Ok(constructs);
}
pub fn team(tx: &mut Transaction, account: &Account) -> Result<Vec<Construct>, 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<u8> = row.get(0);
match from_slice::<ConstructSkeleton>(&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::<Vec<Construct>>();
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<Uuid>) -> Result<Vec<Construct>, 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<Vec<Instance>, 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<u8> = row.get(0);
let id = row.get(1);
match from_slice::<Instance>(&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<Uuid, Error> {
match account.subscribed {
true => match img::exists(account.img) {
true => Ok(account.img),
false => img::shapes_write(account.img)
},
false => Ok(account.img),
}
}
pub fn tutorial(tx: &mut Transaction, account: &Account) -> Result<Option<Instance>, Error> {
let query = "
SELECT count(id)
FROM players
WHERE account = $1;
";
let result = tx
.query(query, &[&account.id])?;
let row = result.iter().next()
.ok_or(format_err!("unable to fetch joined games account={:?}", account))?;
let count: i64 = row.get(0);
if count == 0 {
return Ok(Some(instance::instance_practice(tx, account)?));
}
return Ok(None);
}