252 lines
5.8 KiB
Rust
252 lines
5.8 KiB
Rust
use uuid::Uuid;
|
|
use bcrypt::{hash, verify};
|
|
use rand::{thread_rng, Rng};
|
|
use rand::distributions::Alphanumeric;
|
|
use std::iter;
|
|
use serde_cbor::{from_slice};
|
|
|
|
use postgres::transaction::Transaction;
|
|
|
|
use rpc::{AccountCreateParams, AccountLoginParams};
|
|
|
|
use cryp::{Cryp, CrypRecover, cryp_write};
|
|
use game::Game;
|
|
use zone::{Zone, zone_delete};
|
|
|
|
use failure::Error;
|
|
use failure::err_msg;
|
|
|
|
static PASSWORD_MIN_LEN: usize = 12;
|
|
|
|
#[derive(Debug,Clone,Serialize,Deserialize)]
|
|
pub struct Account {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
token: String,
|
|
}
|
|
|
|
#[derive(Debug,Clone,Serialize,Deserialize)]
|
|
struct AccountEntry {
|
|
id: Uuid,
|
|
name: String,
|
|
password: String,
|
|
token: String,
|
|
}
|
|
|
|
// MAYBE
|
|
// hash tokens with a secret
|
|
pub fn account_from_token(token: String, tx: &mut Transaction) -> Result<Account, Error> {
|
|
let query = "
|
|
SELECT id, name, token
|
|
FROM accounts
|
|
WHERE token = $1;
|
|
";
|
|
|
|
let result = tx
|
|
.query(query, &[&token])?;
|
|
|
|
let returned = match result.iter().next() {
|
|
Some(row) => row,
|
|
None => return Err(err_msg("invalid token")),
|
|
};
|
|
|
|
let entry = Account {
|
|
id: returned.get(0),
|
|
name: returned.get(1),
|
|
token: returned.get(2),
|
|
};
|
|
|
|
return Ok(entry);
|
|
}
|
|
|
|
pub fn account_create(params: AccountCreateParams, tx: &mut Transaction) -> Result<Account, Error> {
|
|
let id = Uuid::new_v4();
|
|
|
|
if params.password.len() < PASSWORD_MIN_LEN {
|
|
return Err(err_msg("password must be at least 12 characters"));
|
|
}
|
|
|
|
let rounds = 8;
|
|
let password = hash(¶ms.password, rounds)?;
|
|
|
|
let mut rng = thread_rng();
|
|
let token: String = iter::repeat(())
|
|
.map(|()| rng.sample(Alphanumeric))
|
|
.take(64)
|
|
.collect();
|
|
|
|
let account = AccountEntry {
|
|
name: params.name,
|
|
id,
|
|
password,
|
|
token,
|
|
};
|
|
|
|
let query = "
|
|
INSERT INTO accounts (id, name, password, token)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, name, token;
|
|
";
|
|
|
|
|
|
let result = tx
|
|
.query(query, &[&account.id, &account.name, &account.password, &account.token])?;
|
|
|
|
let returned = result.iter().next().expect("no row returned");
|
|
|
|
let entry = Account {
|
|
id: returned.get(0),
|
|
name: returned.get(1),
|
|
token: returned.get(2),
|
|
};
|
|
|
|
println!("{:?} registered", entry.name);
|
|
|
|
return Ok(entry);
|
|
}
|
|
|
|
pub fn account_login(params: AccountLoginParams, tx: &mut Transaction) -> Result<Account, Error> {
|
|
let query = "
|
|
SELECT id, name, token, password
|
|
FROM accounts
|
|
WHERE name = $1;
|
|
";
|
|
|
|
let result = tx
|
|
.query(query, &[¶ms.name])?;
|
|
|
|
let returned = match result.iter().next() {
|
|
Some(row) => row,
|
|
// MAYBE
|
|
// verify gibberish to delay response for timing attacks
|
|
None => return Err(err_msg("account not found")),
|
|
};
|
|
|
|
let entry = AccountEntry {
|
|
id: returned.get(0),
|
|
name: returned.get(1),
|
|
token: returned.get(2),
|
|
password: returned.get(3),
|
|
};
|
|
|
|
if !verify(¶ms.password, &entry.password)? {
|
|
return Err(err_msg("password does not match"));
|
|
}
|
|
|
|
println!("{:?} logged in", entry.name);
|
|
|
|
// MAYBE
|
|
// update token?
|
|
// don't necessarily want to log every session out when logging in
|
|
|
|
let account = Account {
|
|
id: entry.id,
|
|
name: entry.name,
|
|
token: entry.token,
|
|
};
|
|
|
|
return Ok(account);
|
|
}
|
|
|
|
fn recover_cryp(cryp_bytes: Vec<u8>, tx: &mut Transaction) -> Result<Cryp, Error> {
|
|
let c = from_slice::<CrypRecover>(&cryp_bytes)?;
|
|
|
|
let mut cryp = Cryp::new()
|
|
.named(&c.name)
|
|
.level(c.lvl)
|
|
.set_account(c.account)
|
|
.create();
|
|
|
|
cryp.id = c.id;
|
|
|
|
println!("recovered cryp {:?}", c.name);
|
|
|
|
return cryp_write(cryp, tx);
|
|
}
|
|
|
|
pub fn account_cryps(tx: &mut Transaction, account: &Account) -> Result<Vec<Cryp>, Error> {
|
|
let query = "
|
|
SELECT data
|
|
FROM cryps
|
|
WHERE account = $1;
|
|
";
|
|
|
|
let result = tx
|
|
.query(query, &[&account.id])?;
|
|
|
|
let cryps: Result<Vec<Cryp>, _> = result.iter()
|
|
.map(|row| {
|
|
let cryp_bytes: Vec<u8> = row.get(0);
|
|
match from_slice::<Cryp>(&cryp_bytes) {
|
|
Ok(c) => Ok(c),
|
|
Err(_e) => recover_cryp(cryp_bytes, tx),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
// catch any errors
|
|
if cryps.is_err() {
|
|
return Err(err_msg("could not deserialize a cryp"));
|
|
}
|
|
|
|
// now unwrap is safe
|
|
return Ok(cryps.unwrap());
|
|
}
|
|
|
|
pub fn account_game_history(tx: &mut Transaction, account: &Account) -> Result<Vec<Game>, Error> {
|
|
let query = "
|
|
SELECT games.data
|
|
FROM players join games
|
|
ON (players.game = games.id)
|
|
WHERE account = $1;
|
|
";
|
|
|
|
let result = tx
|
|
.query(query, &[&account.id])?;
|
|
|
|
let games: Result<Vec<Game>, _> = result.iter().map(|row| {
|
|
let cryp_bytes: Vec<u8> = row.get(0);
|
|
from_slice::<Game>(&cryp_bytes)
|
|
}).collect();
|
|
|
|
// catch any errors
|
|
if games.is_err() {
|
|
return Err(err_msg("could not deserialize a game"));
|
|
}
|
|
|
|
// now unwrap is safe
|
|
return Ok(games.unwrap());
|
|
|
|
}
|
|
|
|
pub fn account_zone(tx: &mut Transaction, account: &Account) -> Result<Zone, Error> {
|
|
let query = "
|
|
SELECT *
|
|
FROM zones
|
|
WHERE account = $1
|
|
AND active = true;
|
|
";
|
|
|
|
let result = tx
|
|
.query(query, &[&account.id])?;
|
|
|
|
let returned = match result.iter().next() {
|
|
Some(row) => row,
|
|
None => return Err(err_msg("no active zone")),
|
|
};
|
|
|
|
// tells from_slice to cast into a cryp
|
|
let bytes: Vec<u8> = returned.get("data");
|
|
let zone = match from_slice::<Zone>(&bytes) {
|
|
Ok(z) => z,
|
|
Err(_) => {
|
|
zone_delete(tx, returned.get("id"))?;
|
|
return Err(err_msg("invalid zone removed"))
|
|
},
|
|
};
|
|
|
|
return Ok(zone);
|
|
}
|
|
|
|
|