password change

This commit is contained in:
ntr 2019-08-13 12:26:32 +10:00
parent 0507381e20
commit f0166a1779
4 changed files with 217 additions and 72 deletions

View File

@ -1,9 +1,11 @@
const { connect } = require('preact-redux');
const preact = require('preact'); const preact = require('preact');
const { Component } = require('preact')
const { connect } = require('preact-redux');
const linkState = require('linkstate').default;
const SpawnButton = require('./spawn.button'); const SpawnButton = require('./spawn.button');
const { postData } = require('./../utils'); const { postData, errorToast, infoToast } = require('../utils');
const actions = require('../actions'); const actions = require('../actions');
const addState = connect( const addState = connect(
@ -15,10 +17,22 @@ const addState = connect(
} = state; } = 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() { function logout() {
postData('/account/logout').then(() => window.location.reload(true)); postData('/account/logout').then(() => window.location.reload(true));
} }
function sendConstructSpawn(name) { function sendConstructSpawn(name) {
return ws.sendMtxConstructSpawn(name); return ws.sendMtxConstructSpawn(name);
} }
@ -27,22 +41,41 @@ const addState = connect(
account, account,
ping, ping,
logout, logout,
setPassword,
sendConstructSpawn, sendConstructSpawn,
}; };
}, },
); );
function AccountStatus(args) { class AccountStatus extends Component {
constructor(props) {
super(props);
this.state = {
setPassword: { current: '', password: '', confirm: ''},
};
}
render(args) {
const { const {
account, account,
ping, ping,
logout, logout,
setPassword,
sendConstructSpawn, sendConstructSpawn,
} = args; } = args;
if (!account) return null; if (!account) return null;
const passwordsEqual = () =>
this.state.setPassword.password === this.state.setPassword.confirm;
const setPasswordDisabled = () => {
const { current, password, confirm } = this.state.setPassword;
return !(passwordsEqual() && password && current && confirm);
}
return ( return (
<section class='account'> <section class='account'>
<div> <div>
@ -53,7 +86,7 @@ function AccountStatus(args) {
</dl> </dl>
<button onClick={() => logout()}>Logout</button> <button onClick={() => logout()}>Logout</button>
</div> </div>
<form> <div>
<label for="email">Update Email:</label> <label for="email">Update Email:</label>
<input <input
class="login-input" class="login-input"
@ -69,29 +102,39 @@ function AccountStatus(args) {
placeholder="new email" placeholder="new email"
/> />
<button>Update</button> <button>Update</button>
</form> </div>
<form> <div>
<label for="current">Change Password:</label> <label for="current">Change Password:</label>
<input <input
class="login-input" class="login-input"
type="password" type="password"
name="current" name="current"
value={this.state.setPassword.current}
onInput={linkState(this, 'setPassword.current')}
placeholder="current" placeholder="current"
/> />
<input <input
class="login-input" class="login-input"
type="password" type="password"
name="new" name="new"
value={this.state.setPassword.password}
onInput={linkState(this, 'setPassword.password')}
placeholder="new password" placeholder="new password"
/> />
<input <input
class="login-input" class="login-input"
type="password" type="password"
name="confirm" name="confirm"
value={this.state.setPassword.confirm}
onInput={linkState(this, 'setPassword.confirm')}
placeholder="confirm" placeholder="confirm"
/> />
<button>Change</button> <button
</form> disabled={setPasswordDisabled()}
onClick={() => setPassword(this.state.setPassword.current, this.state.setPassword.password)}>
Set Password
</button>
</div>
<div class="list"> <div class="list">
<figure> <figure>
<figcaption>spawn new construct</figcaption> <figcaption>spawn new construct</figcaption>
@ -103,5 +146,6 @@ function AccountStatus(args) {
</section> </section>
); );
} }
}
module.exports = addState(AccountStatus); module.exports = addState(AccountStatus);

View File

@ -213,6 +213,15 @@ function errorToast(message) {
}); });
} }
function infoToast(message) {
toast.info({
position: 'topRight',
drag: false,
close: false,
message,
});
}
function convertItem(v) { function convertItem(v) {
if (['Red', 'Green', 'Blue'].includes(v)) { if (['Red', 'Green', 'Blue'].includes(v)) {
return ( return (
@ -233,6 +242,7 @@ module.exports = {
postData, postData,
convertItem, convertItem,
errorToast, errorToast,
infoToast,
NULL_UUID, NULL_UUID,
STATS, STATS,
COLOURS, COLOURS,

View File

@ -117,7 +117,7 @@ pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result<A
Ok(Account { id, name, balance, subscribed }) Ok(Account { id, name, balance, subscribed })
} }
pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, Error> { pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, MnmlHttpError> {
let mut rng = thread_rng(); let mut rng = thread_rng();
let token: String = iter::repeat(()) let token: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric)) .map(|()| rng.sample(Alphanumeric))
@ -136,11 +136,73 @@ pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, Error> {
.query(query, &[&token, &id])?; .query(query, &[&token, &id])?;
result.iter().next() result.iter().next()
.ok_or(format_err!("account not updated {:?}", id))?; .ok_or(MnmlHttpError::Unauthorized)?;
Ok(token) Ok(token)
} }
pub fn set_password(tx: &mut Transaction, id: Uuid, current: &String, password: &String) -> Result<String, MnmlHttpError> {
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<String, Error> { pub fn credit(tx: &mut Transaction, id: Uuid, credits: i64) -> Result<String, Error> {
let query = " let query = "
UPDATE accounts UPDATE accounts

View File

@ -221,7 +221,7 @@ fn login(req: &mut Request) -> IronResult<Response> {
match account::login(&mut tx, &params.name, &params.password) { match account::login(&mut tx, &params.name, &params.password) {
Ok(a) => { 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))?; tx.commit().or(Err(MnmlHttpError::ServerError))?;
Ok(token_res(token)) Ok(token_res(token))
}, },
@ -239,7 +239,7 @@ fn logout(req: &mut Request) -> IronResult<Response> {
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
let mut tx = db.transaction().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))?; tx.commit().or(Err(MnmlHttpError::ServerError))?;
@ -252,6 +252,33 @@ fn logout(req: &mut Request) -> IronResult<Response> {
} }
} }
#[derive(Debug,Clone,Deserialize)]
struct SetPassword {
current: String,
password: String,
}
fn set_password(req: &mut Request) -> IronResult<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<SetPassword>>() {
Ok(Some(b)) => b,
_ => return Err(IronError::from(MnmlHttpError::BadRequest)),
};
match req.extensions.get::<account::Account>() {
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, &params.current, &params.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; 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/login", login, "login");
router.post("/api/account/logout", logout, "logout"); router.post("/api/account/logout", logout, "logout");
router.post("/api/account/register", register, "register"); router.post("/api/account/register", register, "register");
router.post("/api/account/password", set_password, "set_password");
router.post("/api/account/email", logout, "email");
// payments // payments
router.post("/api/payments/stripe", stripe, "stripe"); router.post("/api/payments/stripe", stripe, "stripe");