553 lines
14 KiB
Rust
553 lines
14 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 failure::Error;
|
|
use failure::{err_msg, format_err};
|
|
|
|
use mnml_core::construct::{Construct, ConstructSkeleton};
|
|
use mnml_core::instance::{Instance};
|
|
use mnml_core::player::{Player};
|
|
|
|
use http::MnmlHttpError;
|
|
use names::{name as generate_name};
|
|
use mtx::{Mtx, FREE_MTX};
|
|
use pg::{Db, instance_delete, construct_spawn, instance_practice};
|
|
use img;
|
|
|
|
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 Account {
|
|
pub fn to_player(&self, tx: &mut Transaction) -> Result<Player, Error> {
|
|
let constructs = team(tx, self)?;
|
|
|
|
let img = match self.subscribed {
|
|
true => Some(self.img),
|
|
false => None,
|
|
};
|
|
|
|
Ok(Player::new(self.id, Some(self.img), &self.name, constructs))
|
|
}
|
|
|
|
pub fn anonymous() -> Account {
|
|
Account {
|
|
id: Uuid::new_v4(),
|
|
img: Uuid::new_v4(),
|
|
name: "you".to_string(),
|
|
balance: 0,
|
|
subscribed: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
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_practice(tx, account)?));
|
|
}
|
|
|
|
return Ok(None);
|
|
} |