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),