mnml/server/src/account.rs
2019-06-28 16:08:55 +10:00

300 lines
8.2 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 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<Account, Error> {
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<Account, Error> {
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<Account, Error> {
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<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, &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<String, Error> {
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<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 account_create(name: &String, password: &String, code: &String, tx: &mut Transaction) -> Result<String, Error> {
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<Vec<Construct>, Error> {
let query = "
SELECT data
FROM constructs
WHERE account = $1;
";
let result = tx
.query(query, &[&account.id])?;
let constructs: Result<Vec<Construct>, _> = result.iter()
.map(|row| {
let construct_bytes: Vec<u8> = row.get(0);
match from_slice::<Construct>(&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<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);
}