diff --git a/client/src/components/account.management.jsx b/client/src/components/account.management.jsx index 792ac852..fe058f6f 100644 --- a/client/src/components/account.management.jsx +++ b/client/src/components/account.management.jsx @@ -1,9 +1,11 @@ -const { connect } = require('preact-redux'); const preact = require('preact'); +const { Component } = require('preact') +const { connect } = require('preact-redux'); +const linkState = require('linkstate').default; const SpawnButton = require('./spawn.button'); -const { postData } = require('./../utils'); +const { postData, errorToast, infoToast } = require('../utils'); const actions = require('../actions'); const addState = connect( @@ -15,10 +17,22 @@ const addState = connect( } = state; + function setPassword(current, password) { + postData('/account/password', { current, password }) + .then(res => res.json()) + .then(data => { + if (!data.success) return errorToast(data.error_message); + infoToast('Password changed. Reloading...') + setTimeout(() => window.location.reload(), 5000); + }) + .catch(error => errorToast(error)); + } + function logout() { postData('/account/logout').then(() => window.location.reload(true)); } + function sendConstructSpawn(name) { return ws.sendMtxConstructSpawn(name); } @@ -27,81 +41,111 @@ const addState = connect( account, ping, logout, + setPassword, sendConstructSpawn, }; }, ); -function AccountStatus(args) { - const { - account, - ping, - logout, - sendConstructSpawn, - } = args; +class AccountStatus extends Component { + constructor(props) { + super(props); - if (!account) return null; + this.state = { + setPassword: { current: '', password: '', confirm: ''}, + }; + } - return ( -
-
-

{account.name}

-
-
Subscription
-
{account.subscribed ? 'some date' : 'unsubscribed'}
-
- -
-
- - - - -
-
- - - - - -
-
-
-
spawn new construct
- +
+
+ + + + +
+
+ + + + + - -
-
- ); + +
+
+
spawn new construct
+ +
+
+ + ); + } } module.exports = addState(AccountStatus); diff --git a/client/src/utils.jsx b/client/src/utils.jsx index 1aa57056..a50ba095 100644 --- a/client/src/utils.jsx +++ b/client/src/utils.jsx @@ -213,6 +213,15 @@ function errorToast(message) { }); } +function infoToast(message) { + toast.info({ + position: 'topRight', + drag: false, + close: false, + message, + }); +} + function convertItem(v) { if (['Red', 'Green', 'Blue'].includes(v)) { return ( @@ -233,6 +242,7 @@ module.exports = { postData, convertItem, errorToast, + infoToast, NULL_UUID, STATS, COLOURS, diff --git a/server/src/account.rs b/server/src/account.rs index 16833c3a..06c62e3f 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -117,7 +117,7 @@ pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result Result { +pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result { let mut rng = thread_rng(); let token: String = iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) @@ -136,11 +136,73 @@ pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result { .query(query, &[&token, &id])?; result.iter().next() - .ok_or(format_err!("account not updated {:?}", id))?; + .ok_or(MnmlHttpError::Unauthorized)?; Ok(token) } +pub fn set_password(tx: &mut Transaction, id: Uuid, current: &String, password: &String) -> Result { + if password.len() < PASSWORD_MIN_LEN { + 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); + + if !verify(current, &db_pw)? { + return Err(MnmlHttpError::PasswordNotMatch); + } + + let rounds = 8; + let password = hash(&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 { let query = " UPDATE accounts diff --git a/server/src/net.rs b/server/src/net.rs index 0e818e16..31f0e4c8 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -221,7 +221,7 @@ fn login(req: &mut Request) -> IronResult { match account::login(&mut tx, ¶ms.name, ¶ms.password) { Ok(a) => { - let token = account::new_token(&mut tx, a.id).or(Err(MnmlHttpError::ServerError))?; + let token = account::new_token(&mut tx, a.id)?; tx.commit().or(Err(MnmlHttpError::ServerError))?; Ok(token_res(token)) }, @@ -239,7 +239,7 @@ fn logout(req: &mut Request) -> IronResult { let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; - account::new_token(&mut tx, a.id).or(Err(MnmlHttpError::Unauthorized))?; + account::new_token(&mut tx, a.id)?; tx.commit().or(Err(MnmlHttpError::ServerError))?; @@ -252,6 +252,33 @@ fn logout(req: &mut Request) -> IronResult { } } +#[derive(Debug,Clone,Deserialize)] +struct SetPassword { + current: String, + password: String, +} + +fn set_password(req: &mut Request) -> IronResult { + let state = req.get::>().unwrap(); + let params = match req.get::>() { + Ok(Some(b)) => b, + _ => return Err(IronError::from(MnmlHttpError::BadRequest)), + }; + + match req.extensions.get::() { + Some(a) => { + let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; + let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; + + let token = account::set_password(&mut tx, a.id, ¶ms.current, ¶ms.password)?; + + tx.commit().or(Err(MnmlHttpError::ServerError))?; + + Ok(token_res(token)) + }, + None => Err(IronError::from(MnmlHttpError::Unauthorized)), + } +} const MAX_BODY_LENGTH: usize = 1024 * 1024 * 10; @@ -269,6 +296,8 @@ pub fn start(pool: PgPool) { router.post("/api/account/login", login, "login"); router.post("/api/account/logout", logout, "logout"); router.post("/api/account/register", register, "register"); + router.post("/api/account/password", set_password, "set_password"); + router.post("/api/account/email", logout, "email"); // payments router.post("/api/payments/stripe", stripe, "stripe");