[Addition] E-mail confirmations for user actions

* If enabled, sends e-mail to confirm user withdraws, edits and pw changes
 * Adds 4 config options, enabled + individual settings
 * Adds 3 new token_types
This commit is contained in:
xisi 2014-01-15 02:49:58 -05:00
parent 409f41bc35
commit ef904858ae
11 changed files with 172 additions and 10 deletions

View File

@ -32,10 +32,36 @@ class Payout Extends Base {
/**
* Insert a new payout request
* @param account_id Account ID
* @param account_id int Account ID
* @param strToken string Token to confirm
* @return data mixed Inserted ID or false
**/
public function createPayout($account_id=NULL) {
public function createPayout($account_id=NULL, $strToken) {
// twofactor - if cashout enabled we need to create/check the token
if ($this->config['twofactor']['enabled'] && $this->config['twofactor']['withdraw']) {
$tData = $this->token->getToken($strToken, 'withdraw_funds');
$tExists = $this->token->doesTokenExist('withdraw_funds', $account_id);
if (!is_array($tData) && $tExists == false) {
// token doesn't exist, let's create one, send an email with a link to use it, and error out
$token = $this->token->createToken('withdraw_funds', $account_id);
$aData['token'] = $token;
$aData['username'] = $this->getUserName($account_id);
$aData['email'] = $this->getUserEmail($aData['username']);
$aData['subject'] = 'Manual payout request confirmation';
$this->mail->sendMail('notifications/withdraw_funds', $aData);
$this->setErrorMessage("A confirmation has been sent to your e-mail");
return false;
} else {
// already exists, if it's valid delete it and allow this edit
if ($strToken === $tData['token']) {
$this->token->deleteToken($tData['token']);
} else {
// token exists for this type, but this is not the right token
$this->setErrorMessage("A confirmation was sent to your e-mail, follow that link to cash out");
return false;
}
}
}
$stmt = $this->mysqli->prepare("INSERT INTO $this->table (account_id) VALUES (?)");
if ($stmt && $stmt->bind_param('i', $account_id) && $stmt->execute()) {
return $stmt->insert_id;
@ -59,6 +85,9 @@ class Payout Extends Base {
$oPayout = new Payout();
$oPayout->setDebug($debug);
$oPayout->setMysql($mysqli);
$oPayout->setConfig($config);
$oPayout->setMail($mail);
$oPayout->setToken($oToken);
$oPayout->setErrorCodes($aErrorCodes);
?>

View File

@ -21,6 +21,23 @@ class Token Extends Base {
return $result->fetch_assoc();
return $this->sqlError();
}
/**
* Check if a token of this type already exists for a given account_id
* @param strType string Name of the type of token
* @param account_id int Account id of user to check
* @return mixed Number of rows on success, false on failure
*/
public function doesTokenExist($strType=NULL, $account_id=NULL) {
if (!$iToken_id = $this->tokentype->getTypeId($strType)) {
$this->setErrorMessage('Invalid token type: ' . $strType);
return false;
}
$stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE account_id = ? AND type = ? LIMIT 1");
if ($stmt && $stmt->bind_param('ii', $account_id, $iToken_id) && $stmt->execute())
return $stmt->num_rows;
return $this->sqlError();
}
/**
* Insert a new token

View File

@ -261,9 +261,10 @@ class User extends Base {
* @param current string Current password
* @param new1 string New password
* @param new2 string New password confirmation
* @param strToken string Token for confirmation
* @return bool
**/
public function updatePassword($userID, $current, $new1, $new2) {
public function updatePassword($userID, $current, $new1, $new2, $strToken) {
$this->debug->append("STA " . __METHOD__, 4);
if ($new1 !== $new2) {
$this->setErrorMessage( 'New passwords do not match' );
@ -273,6 +274,31 @@ class User extends Base {
$this->setErrorMessage( 'New password is too short, please use more than 8 chars' );
return false;
}
// twofactor - if changepw is enabled we need to create/check the token
if ($this->config['twofactor']['enabled'] && $this->config['twofactor']['changepw']) {
$tData = $this->token->getToken($strToken, 'change_pw');
$tExists = $this->token->doesTokenExist('change_pw', $userID);
if (!is_array($tData) && $tExists == false) {
// token doesn't exist, let's create one, send an email with a link to use it, and error out
$token = $this->token->createToken('change_pw', $userID);
$aData['token'] = $token;
$aData['username'] = $this->getUserName($userID);
$aData['email'] = $this->getUserEmail($aData['username']);
$aData['subject'] = 'Account password change confirmation';
$this->mail->sendMail('notifications/change_pw', $aData);
$this->setErrorMessage("A confirmation has been sent to your e-mail");
return false;
} else {
// already exists, if it's valid delete it and allow this edit
if ($strToken === $tData['token']) {
$this->token->deleteToken($tData['token']);
} else {
// token exists for this type, but this is not the right token
$this->setErrorMessage("A confirmation was sent to your e-mail, follow that link to change your password");
return false;
}
}
}
$current = $this->getHash($current);
$new = $this->getHash($new1);
$stmt = $this->mysqli->prepare("UPDATE $this->table SET pass = ? WHERE ( id = ? AND pass = ? )");
@ -294,12 +320,12 @@ class User extends Base {
* @param address string new coin address
* @param threshold float auto payout threshold
* @param donat float donation % of income
* @param strToken string Token for confirmation
* @return bool
**/
public function updateAccount($userID, $address, $threshold, $donate, $email, $is_anonymous) {
public function updateAccount($userID, $address, $threshold, $donate, $email, $is_anonymous, $strToken) {
$this->debug->append("STA " . __METHOD__, 4);
$bUser = false;
// number validation checks
if (!is_numeric($threshold)) {
$this->setErrorMessage('Invalid input for auto-payout');
@ -347,6 +373,32 @@ class User extends Base {
$threshold = min($this->config['ap_threshold']['max'], max(0, floatval($threshold)));
$donate = min(100, max(0, floatval($donate)));
// twofactor - if details enabled we need to create/check the token
if ($this->config['twofactor']['enabled'] && $this->config['twofactor']['details']) {
$tData = $this->token->getToken($strToken, 'account_edit');
$tExists = $this->token->doesTokenExist('account_edit', $userID);
if (!is_array($tData) && $tExists == false) {
// token doesn't exist, let's create one, send an email with a link to use it, and error out
$token = $this->token->createToken('account_edit', $userID);
$aData['token'] = $token;
$aData['username'] = $this->getUserName($userID);
$aData['email'] = $this->getUserEmail($aData['username']);
$aData['subject'] = 'Account detail change confirmation';
$this->mail->sendMail('notifications/account_edit', $aData);
$this->setErrorMessage("A confirmation has been sent to your e-mail");
return false;
} else {
// already exists, if it's valid delete it and allow this edit
if ($strToken === $tData['token']) {
$this->token->deleteToken($tData['token']);
} else {
// token exists for this type, but this is not the right token
$this->setErrorMessage("A confirmation was sent to your e-mail, follow that link to edit your account details");
return false;
}
}
}
// We passed all validation checks so update the account
$stmt = $this->mysqli->prepare("UPDATE $this->table SET coin_address = ?, ap_threshold = ?, donate_percent = ?, email = ?, is_anonymous = ? WHERE id = ?");
if ($this->checkStmt($stmt) && $stmt->bind_param('sddsii', $address, $threshold, $donate, $email, $is_anonymous, $userID) && $stmt->execute())

View File

@ -99,6 +99,30 @@ $config['coldwallet']['address'] = '';
$config['coldwallet']['reserve'] = 50;
$config['coldwallet']['threshold'] = 5;
/**
* E-mail confirmations for user actions
*
* Explanation:
* To increase security for users, account detail changes can require
* an e-mail confirmation prior to performing certain actions.
*
* Options:
* enabled : Whether or not to require e-mail confirmations
* details : Require confirmation to change account details
* withdraw : Require confirmation to manually withdraw/payout
* changepw : Require confirmation to change password
*
* Default:
* enabled = true
* details = true
* withdraw = true
* changepw = true
*/
$config['twofactor']['enabled'] = true;
$config['twofactor']['details'] = true;
$config['twofactor']['withdraw'] = true;
$config['twofactor']['changepw'] = true;
/**
* Lock account after maximum failed logins
*

View File

@ -25,10 +25,11 @@ if ($user->isAuthenticated()) {
$dBalance = $aBalance['confirmed'];
if ($dBalance > $config['txfee_manual']) {
if (!$oPayout->isPayoutActive($_SESSION['USERDATA']['id'])) {
if ($iPayoutId = $oPayout->createPayout($_SESSION['USERDATA']['id'])) {
$wf_token = (!isset($_POST['wf_token'])) ? '' : $_POST['wf_token'];
if ($iPayoutId = $oPayout->createPayout($_SESSION['USERDATA']['id'], $wf_token)) {
$_SESSION['POPUP'][] = array('CONTENT' => 'Created new manual payout request with ID #' . $iPayoutId);
} else {
$_SESSION['POPUP'][] = array('CONTENT' => 'Failed to create manual payout request.', 'TYPE' => 'errormsg');
$_SESSION['POPUP'][] = array('CONTENT' => $iPayoutId->getError(), 'TYPE' => 'errormsg');
}
} else {
$_SESSION['POPUP'][] = array('CONTENT' => 'You already have one active manual payout request.', 'TYPE' => 'errormsg');
@ -40,7 +41,8 @@ if ($user->isAuthenticated()) {
break;
case 'updateAccount':
if ($user->updateAccount($_SESSION['USERDATA']['id'], $_POST['paymentAddress'], $_POST['payoutThreshold'], $_POST['donatePercent'], $_POST['email'], $_POST['is_anonymous'])) {
$ea_token = (!isset($_POST['ea_token'])) ? '' : $_POST['ea_token'];
if ($user->updateAccount($_SESSION['USERDATA']['id'], $_POST['paymentAddress'], $_POST['payoutThreshold'], $_POST['donatePercent'], $_POST['email'], $_POST['is_anonymous'], $ea_token)) {
$_SESSION['POPUP'][] = array('CONTENT' => 'Account details updated', 'TYPE' => 'success');
} else {
$_SESSION['POPUP'][] = array('CONTENT' => 'Failed to update your account: ' . $user->getError(), 'TYPE' => 'errormsg');
@ -48,7 +50,8 @@ if ($user->isAuthenticated()) {
break;
case 'updatePassword':
if ($user->updatePassword($_SESSION['USERDATA']['id'], $_POST['currentPassword'], $_POST['newPassword'], $_POST['newPassword2'])) {
$cp_token = (!isset($_POST['cp_token'])) ? '' : $_POST['cp_token'];
if ($user->updatePassword($_SESSION['USERDATA']['id'], $_POST['currentPassword'], $_POST['newPassword'], $_POST['newPassword2'], $cp_token)) {
$_SESSION['POPUP'][] = array('CONTENT' => 'Password updated', 'TYPE' => 'success');
} else {
$_SESSION['POPUP'][] = array('CONTENT' => $user->getError(), 'TYPE' => 'errormsg');

View File

@ -0,0 +1,9 @@
<html>
<body>
<p>You have a pending request to change your account details.</p>
<p>If you initiated this request, please follow the link below to confirm your changes. If you did NOT, please notify an administrator.</p>
<p>http://{$smarty.server.SERVER_NAME}{$smarty.server.SCRIPT_NAME}?page=account&action=edit&ea_token={nocache}{$DATA.token}{/nocache}</p>
<br/>
<br/>
</body>
</html>

View File

@ -0,0 +1,9 @@
<html>
<body>
<p>You have a pending request to change your password.</p>
<p>If you initiated this request, please follow the link below to confirm your changes. If you did NOT, please notify an administrator.</p>
<p>http://{$smarty.server.SERVER_NAME}{$smarty.server.SCRIPT_NAME}?page=account&action=edit&cp_token={nocache}{$DATA.token}{/nocache}</p>
<br/>
<br/>
</body>
</html>

View File

@ -0,0 +1,9 @@
<html>
<body>
<p>You have a pending request to manually withdraw funds.</p>
<p>If you initiated this request, please follow the link below to confirm your changes. If you did NOT, please notify an administrator.</p>
<p>http://{$smarty.server.SERVER_NAME}{$smarty.server.SCRIPT_NAME}?page=account&action=edit&wf_token={nocache}{$DATA.token}{/nocache}</p>
<br/>
<br/>
</body>
</html>

View File

@ -55,6 +55,7 @@
</div>
<footer>
<div class="submit_link">
<input type="hidden" name="ea_token" value="{$smarty.request.ea_token|escape}">
<input type="submit" value="Update Account" class="alt_btn">
</div>
</footer>
@ -89,6 +90,7 @@
</div>
<footer>
<div class="submit_link">
<input type="hidden" name="wf_token" value="{$smarty.request.wf_token|escape}">
<input type="submit" value="Cash Out" class="alt_btn">
</div>
</footer>
@ -127,6 +129,7 @@
</div>
<footer>
<div class="submit_link">
<input type="hidden" name="cp_token" value="{$smarty.request.cp_token|escape}">
<input type="submit" value="Change Password" class="alt_btn">
</div>
</footer>

View File

@ -200,7 +200,10 @@ INSERT INTO `token_types` (`id`, `name`, `expiration`) VALUES
(1, 'password_reset', 3600),
(2, 'confirm_email', 0),
(3, 'invitation', 0),
(4, 'account_unlock', 0);
(4, 'account_unlock', 0),
(5, 'account_edit', 360),
(6, 'change_pw', 360),
(7, 'withdraw_funds', 360);
CREATE TABLE IF NOT EXISTS `transactions` (
`id` int(255) NOT NULL AUTO_INCREMENT,
@ -230,3 +233,4 @@ CREATE TABLE IF NOT EXISTS `templates` (
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@ -0,0 +1,3 @@
INSERT INTO `token_types` (`name`, `expiration`) VALUES ('account_edit', 360);
INSERT INTO `token_types` (`name`, `expiration`) VALUES ('change_pw', 360);
INSERT INTO `token_types` (`name`, `expiration`) VALUES ('withdraw_funds', 360);