diff --git a/client/src/components/main.jsx b/client/src/components/main.jsx index 59747677..235ecf4a 100644 --- a/client/src/components/main.jsx +++ b/client/src/components/main.jsx @@ -10,8 +10,8 @@ const List = require('./list'); const addState = connect( state => { - const { game, instance, account, nav, team } = state; - return { game, instance, account, nav, team }; + const { game, instance, account, nav, team, constructs } = state; + return { game, instance, account, nav, team, constructs }; } ); @@ -22,6 +22,7 @@ function Main(props) { account, nav, team, + constructs, } = props; if (!account) { @@ -36,8 +37,8 @@ function Main(props) { return ; } + if (nav === 'team' || !team.some(t => t) || constructs.length < 3) return ; if (nav === 'list') return ; - if (nav === 'team' || !team.some(t => t)) return ; return (
diff --git a/ops/migrations/20180913000513_create_accounts.js b/ops/migrations/20180913000513_create_accounts.js index 35683160..675bfc7c 100755 --- a/ops/migrations/20180913000513_create_accounts.js +++ b/ops/migrations/20180913000513_create_accounts.js @@ -4,7 +4,8 @@ exports.up = async knex => { table.timestamps(true, true); table.string('name', 42).notNullable().unique(); table.string('password').notNullable(); - table.string('token', 64).notNullable(); + table.string('token', 64); + table.timestamp('token_expiry'); table.index('name'); table.index('id'); diff --git a/server/src/account.rs b/server/src/account.rs index e084ac4a..3b85e4da 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -29,13 +29,12 @@ struct AccountEntry { token: String, } -// MAYBE -// hash tokens with a secret pub fn account_from_token(token: String, tx: &mut Transaction) -> Result { let query = " SELECT id, name, token FROM accounts - WHERE token = $1; + WHERE token = $1 + AND token_expiry > now(); "; let result = tx @@ -71,78 +70,93 @@ pub fn account_create(name: &String, password: &String, code: &String, tx: &mut 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) - VALUES ($1, $2, $3, $4) - RETURNING id, name, token; + INSERT INTO accounts (id, name, password) + VALUES ($1, $2, $3) + RETURNING id, name; "; let result = tx - .query(query, &[&id, &name, &password, &token])?; + .query(query, &[&id, &name, &password])?; - if result.is_empty() { - return Err(err_msg("no row returned")); - } + let returned = match result.iter().next() { + Some(row) => row, + None => return Err(err_msg("account not created")), + }; info!("registration account={:?}", name); - return Ok(token); + let account = Account { + id: returned.get(0), + name: returned.get(1), + }; + + account_set_token(tx, &account) } pub fn account_login(name: &String, password: &String, tx: &mut Transaction) -> Result { let query = " - SELECT id, password + SELECT id, password, name FROM accounts - WHERE name = $1; + WHERE name = $1 + RETURNING id, name; "; let result = tx .query(query, &[&name])?; - let mut rng = thread_rng(); - let token: String = iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) - .take(64) - .collect(); - let returned = 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(token.clone(), &token).ok(); + verify(garbage.clone(), &garbage).ok(); return Err(err_msg("account not found")); }, }; - let id: Uuid = returned.get(0); + let account = Account { + id: returned.get(0), + name: returned.get(2), + }; + let hash: String = returned.get(1); if !verify(password, &hash)? { return Err(err_msg("password does not match")); } + account_set_token(tx, &account) +} + +fn account_set_token(tx: &mut Transaction, account: &Account) -> 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() + SET token = $1, updated_at = now(), token_expiry = now() + interval '1 week' WHERE id = $2 RETURNING id; "; let result = tx - .query(query, &[&token, &id])?; + .query(query, &[&token, &account.id])?; - result.iter().next().ok_or(format_err!("user {:?} could not be updated", id))?; + result.iter().next().ok_or(format_err!("user {:?} could not be updated", account.id))?; - info!("login account={:?}", name); + info!("login account={:?}", account.name); - return Ok(token); + Ok(token) } pub fn account_constructs(tx: &mut Transaction, account: &Account) -> Result, Error> { diff --git a/server/src/net.rs b/server/src/net.rs index d3b9635e..0f4973df 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -26,11 +26,7 @@ const DB_POOL_SIZE: u32 = 20; const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); -/// websocket connection is long running connection, it easier -/// to handle with an actor pub struct MnmlSocket { - /// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT), - /// otherwise we drop connection. hb: Instant, pool: PgPool, account: Option, @@ -170,11 +166,10 @@ fn connect(r: HttpRequest, state: web::Data, stream: web::Payload) -> Res fn token_res(token: String, secure: bool) -> HttpResponse { HttpResponse::Ok() .cookie(Cookie::build("x-auth-token", token) - // .path("/") - .secure(secure) // needs to be enabled for prod + .secure(secure) .http_only(true) .same_site(SameSite::Strict) - .max_age(60 * 60 * 24 * 7) // 1 week + .max_age(60 * 60 * 24 * 7) // 1 week aligns with db set .finish()) .finish() } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index e60bb198..ad359bc2 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -29,7 +29,7 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant, acc let mut tx = db.transaction()?; - let account_name = match &account { + let account_name = match account { Some(a) => a.name.clone(), None => "none".to_string(), }; @@ -37,9 +37,7 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant, acc // check the method // if no auth required match v.method.as_ref() { - "account_create" => (), - // "account_login" => (), - "item_info" => (), + "item_info" => return Ok(RpcResult::ItemInfo(item_info())), _ => match account { Some(_) => (), None => return Err(err_msg("auth required")), @@ -51,9 +49,7 @@ pub fn receive(data: Vec, db: &Db, _client: &mut MnmlWs, begin: Instant, acc // now we have the method name // match on that to determine what fn to call let response = match v.method.as_ref() { - "item_info" => Ok(RpcResult::ItemInfo(item_info())), - "account_state" => Ok(RpcResult::AccountState(account.clone())), - + "account_state" => return Ok(RpcResult::AccountState(account.clone())), "account_constructs" => handle_account_constructs(data, &mut tx, account), "account_instances" => handle_account_instances(data, &mut tx, account),