diff --git a/public/include/classes/payout.class.php b/public/include/classes/payout.class.php index d7c2cae1..96cd5141 100644 --- a/public/include/classes/payout.class.php +++ b/public/include/classes/payout.class.php @@ -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); ?> diff --git a/public/include/classes/token.class.php b/public/include/classes/token.class.php index 8453c245..ca68ebc7 100644 --- a/public/include/classes/token.class.php +++ b/public/include/classes/token.class.php @@ -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 diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index f335f75f..9f026d59 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -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()) diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 85dac842..57ac68ba 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -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 * diff --git a/public/include/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index e368ea92..b69baebd 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -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'); diff --git a/public/templates/mail/notifications/account_edit.tpl b/public/templates/mail/notifications/account_edit.tpl new file mode 100644 index 00000000..fa1fcfcf --- /dev/null +++ b/public/templates/mail/notifications/account_edit.tpl @@ -0,0 +1,9 @@ + + +

You have a pending request to change your account details.

+

If you initiated this request, please follow the link below to confirm your changes. If you did NOT, please notify an administrator.

+

http://{$smarty.server.SERVER_NAME}{$smarty.server.SCRIPT_NAME}?page=account&action=edit&ea_token={nocache}{$DATA.token}{/nocache}

+
+
+ + \ No newline at end of file diff --git a/public/templates/mail/notifications/change_pw.tpl b/public/templates/mail/notifications/change_pw.tpl new file mode 100644 index 00000000..7f0b8f63 --- /dev/null +++ b/public/templates/mail/notifications/change_pw.tpl @@ -0,0 +1,9 @@ + + +

You have a pending request to change your password.

+

If you initiated this request, please follow the link below to confirm your changes. If you did NOT, please notify an administrator.

+

http://{$smarty.server.SERVER_NAME}{$smarty.server.SCRIPT_NAME}?page=account&action=edit&cp_token={nocache}{$DATA.token}{/nocache}

+
+
+ + \ No newline at end of file diff --git a/public/templates/mail/notifications/withdraw_funds.tpl b/public/templates/mail/notifications/withdraw_funds.tpl new file mode 100644 index 00000000..ee17365c --- /dev/null +++ b/public/templates/mail/notifications/withdraw_funds.tpl @@ -0,0 +1,9 @@ + + +

You have a pending request to manually withdraw funds.

+

If you initiated this request, please follow the link below to confirm your changes. If you did NOT, please notify an administrator.

+

http://{$smarty.server.SERVER_NAME}{$smarty.server.SCRIPT_NAME}?page=account&action=edit&wf_token={nocache}{$DATA.token}{/nocache}

+
+
+ + \ No newline at end of file diff --git a/public/templates/mpos/account/edit/default.tpl b/public/templates/mpos/account/edit/default.tpl index 291581f2..1134ec2f 100644 --- a/public/templates/mpos/account/edit/default.tpl +++ b/public/templates/mpos/account/edit/default.tpl @@ -55,6 +55,7 @@ @@ -89,6 +90,7 @@ @@ -127,6 +129,7 @@ diff --git a/sql/000_base_structure.sql b/sql/000_base_structure.sql index 2d6e1789..eebe28ff 100644 --- a/sql/000_base_structure.sql +++ b/sql/000_base_structure.sql @@ -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 */; + diff --git a/sql/013_tokentype_update.sql b/sql/013_tokentype_update.sql new file mode 100644 index 00000000..6c4e5c46 --- /dev/null +++ b/sql/013_tokentype_update.sql @@ -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);