Users: Add ability to change email address in settings

Closes #63
This commit is contained in:
Maxim Leshchenko 2022-05-06 16:17:08 +02:00
parent bfa26c24d1
commit 45fe270700
No known key found for this signature in database
GPG key ID: BB9C44A8733FBEEE
10 changed files with 385 additions and 10 deletions

View file

@ -0,0 +1,204 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Подтверждение изменения Email</title>
<link rel="stylesheet" href="foundation.css" />
<style>
.container {
border-top: 5px solid pink;
}
</style>
</head>
<body>
<table class="body" data-made-with-foundation="">
<tr>
<td class="float-center" align="center" valign="top">
<center>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<table class="container">
<tr>
<td>
<table class="row header">
<tr>
<th class="small-12 large-12 columns first last">
<table>
<tr>
<th>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<h4 class="text-center">Подтверждение изменения Email</h4>
</th>
</tr>
</table>
</th>
</tr>
</table>
<table class="row">
<tr>
<th class="small-12 large-12 columns first last">
<table class="row">
<tr>
<td>
<center>
<img src="pictures/lock.jpeg" align="center" class="float-center" width=128 height=128 />
</center>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<hr/>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<p class="text-left">
Здравствуйте, {$name}! Вы вероятно изменили свой адрес электронной почты в OpenVK. Чтобы изменение вступило в силу, необходимо подтвердить ваш новый Email.
</p>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<table class="button large expand success">
<tr>
<td>
<table>
<tr>
<td>
<center>
<a href="http://{$_SERVER['HTTP_HOST']}/settings/change_email?key={rawurlencode($key)}" align="center" class="float-center">Подтвердить Email!</a>
</center>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<p class="text-left">
Если кнопка не работает, вы можете попробовать скопировать и вставить эту ссылку в адресную строку вашего веб-обозревателя:
</p>
<table class="callout">
<tr>
<th class="callout-inner primary">
<a href="http://{$_SERVER['HTTP_HOST']}/settings/change_email?key={$key}" style="color: #000; text-decoration: none;">
http://{$_SERVER['HTTP_HOST']}/settings/change_email?key={$key}
</a>
</th>
</tr>
</table>
<p class="text-left">
Обратите внимание на то, что эту ссылку нельзя:
</p>
<ul>
<li>Передавать другим людям (даже друзьям, питомцам, соседам, любимым девушкам)</li>
<li>Использовать, если прошло более двух дней с её генерации</li>
</ul>
<table class="callout">
<tr>
<th class="callout-inner alert">
<p>
Ещё раз <b>обратите внимание</b> на то, что данную ссылку или письмо <b>ни в коем случае нельзя</b> передавать другим людям! Даже если они представляются службой поддержки.<br/>
Это письмо предназначено исключительно для одноразового, <b>непосредственного</b> использования владельцем аккаунта.
</p>
</th>
</tr>
</table>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<p class="text-right">
С уважением, овк-тян.
</p>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<hr/>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
<p class="text-left">
<small>
Вы получили это письмо так как кто-то или вы изменили адрес электронной почты. Это не рассылка и от неё нельзя отписаться. Если вы всё равно хотите перестать получать подобные письма, деактивируйте ваш аккаунт.
</small>
</p>
</td>
</tr>
</table>
</th>
</tr>
</table>
</td>
</tr>
</table>
<table class="spacer">
<tr>
<td>
&nbsp;
</td>
</tr>
</table>
</center>
</td>
</tr>
</table>
</body>
</html>

View file

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\RowModel;
use openvk\Web\Util\DateTime;
class EmailChangeVerification extends PasswordReset
{
protected $tableName = "email_change_verifications";
function getNewEmail(): string
{
return $this->getRecord()->new_email;
}
}

View file

@ -877,6 +877,17 @@ class User extends RowModel
return true; return true;
} }
function setEmail(string $email): void
{
DatabaseConnection::i()->getContext()->table("ChandlerUsers")
->where("id", $this->getChandlerUser()->getId())->update([
"login" => $email
]);
$this->stateChanges("email", $email);
$this->save();
}
function adminNotify(string $message): bool function adminNotify(string $message): bool
{ {
$admId = OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"]; $admId = OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["adminAccount"];

View file

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\EmailChangeVerification;
use openvk\Web\Models\Entities\User;
use Nette\Database\Table\ActiveRow;
class EmailChangeVerifications
{
private $context;
private $verifications;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->verifications = $this->context->table("email_change_verifications");
}
function toEmailChangeVerification(?ActiveRow $ar): ?EmailChangeVerification
{
return is_null($ar) ? NULL : new EmailChangeVerification($ar);
}
function getByToken(string $token): ?EmailChangeVerification
{
return $this->toEmailChangeVerification($this->verifications->where("key", $token)->fetch());
}
function getLatestByUser(User $user): ?EmailChangeVerification
{
return $this->toEmailChangeVerification($this->verifications->where("profile", $user->getId())->order("timestamp DESC")->fetch());
}
}

View file

@ -9,12 +9,15 @@ use openvk\Web\Models\Repositories\Albums;
use openvk\Web\Models\Repositories\Videos; use openvk\Web\Models\Repositories\Videos;
use openvk\Web\Models\Repositories\Notes; use openvk\Web\Models\Repositories\Notes;
use openvk\Web\Models\Repositories\Vouchers; use openvk\Web\Models\Repositories\Vouchers;
use openvk\Web\Models\Repositories\EmailChangeVerifications;
use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator; use openvk\Web\Util\Validator;
use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification}; use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification};
use openvk\Web\Models\Entities\EmailChangeVerification;
use Chandler\Security\Authenticator; use Chandler\Security\Authenticator;
use lfkeitel\phptotp\{Base32, Totp}; use lfkeitel\phptotp\{Base32, Totp};
use chillerlan\QRCode\{QRCode, QROptions}; use chillerlan\QRCode\{QRCode, QROptions};
use Nette\Database\UniqueConstraintViolationException;
final class UserPresenter extends OpenVKPresenter final class UserPresenter extends OpenVKPresenter
{ {
@ -132,7 +135,7 @@ final class UserPresenter extends OpenVKPresenter
if(!$id) if(!$id)
$this->notFound(); $this->notFound();
$user = $this->users->get($id); $user = $this->users->get($id);
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
$this->willExecuteWriteAction($_GET['act'] === "status"); $this->willExecuteWriteAction($_GET['act'] === "status");
@ -300,7 +303,7 @@ final class UserPresenter extends OpenVKPresenter
if(!$id) if(!$id)
$this->notFound(); $this->notFound();
if(in_array($this->queryParam("act"), ["finance", "finance.top-up"]) && !OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"]) if(in_array($this->queryParam("act"), ["finance", "finance.top-up"]) && !OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"])
$this->flashFail("err", tr("error"), tr("feature_disabled")); $this->flashFail("err", tr("error"), tr("feature_disabled"));
@ -312,7 +315,7 @@ final class UserPresenter extends OpenVKPresenter
if($this->postParam("old_pass") && $this->postParam("new_pass") && $this->postParam("repeat_pass")) { if($this->postParam("old_pass") && $this->postParam("new_pass") && $this->postParam("repeat_pass")) {
if($this->postParam("new_pass") === $this->postParam("repeat_pass")) { if($this->postParam("new_pass") === $this->postParam("repeat_pass")) {
if($this->user->identity->is2faEnabled()) { if($this->user->identity->is2faEnabled()) {
$code = $this->postParam("code"); $code = $this->postParam("password_change_code");
if(!($code === (new Totp)->GenerateToken(Base32::decode($this->user->identity->get2faSecret())) || $this->user->identity->use2faBackupCode((int) $code))) if(!($code === (new Totp)->GenerateToken(Base32::decode($this->user->identity->get2faSecret())) || $this->user->identity->use2faBackupCode((int) $code)))
$this->flashFail("err", tr("error"), tr("incorrect_2fa_code")); $this->flashFail("err", tr("error"), tr("incorrect_2fa_code"));
} }
@ -323,6 +326,46 @@ final class UserPresenter extends OpenVKPresenter
$this->flashFail("err", tr("error"), tr("error_new_password")); $this->flashFail("err", tr("error"), tr("error_new_password"));
} }
} }
if($this->postParam("new_email")) {
if(!Validator::i()->emailValid($this->postParam("new_email")))
$this->flashFail("err", tr("invalid_email_address"), tr("invalid_email_address_comment"));
if(!Authenticator::verifyHash($this->postParam("email_change_pass"), $user->getChandlerUser()->getRaw()->passwordHash))
$this->flashFail("err", tr("error"), tr("incorrect_password"));
if($user->is2faEnabled()) {
$code = $this->postParam("email_change_code");
if(!($code === (new Totp)->GenerateToken(Base32::decode($user->get2faSecret())) || $user->use2faBackupCode((int) $code)))
$this->flashFail("err", tr("error"), tr("incorrect_2fa_code"));
}
if($this->postParam("new_email") !== $user->getEmail()) {
if (OPENVK_ROOT_CONF['openvk']['preferences']['security']['requireEmail']) {
$request = (new EmailChangeVerifications)->getLatestByUser($user);
if(!is_null($request) && $request->isNew())
$this->flashFail("err", tr("forbidden"), tr("email_rate_limit_error"));
$verification = new EmailChangeVerification;
$verification->setProfile($user->getId());
$verification->setNew_Email($this->postParam("new_email"));
$verification->save();
$params = [
"key" => $verification->getKey(),
"name" => $user->getCanonicalName(),
];
$this->sendmail($this->postParam("new_email"), "change-email", $params); #Vulnerability possible
$this->flashFail("succ", tr("information_-1"), tr("email_change_confirm_message"));
}
try {
$user->setEmail($this->postParam("new_email"));
} catch(UniqueConstraintViolationException $ex) {
$this->flashFail("err", tr("error"), tr("user_already_exists"));
}
}
}
if(!$user->setShortCode(empty($this->postParam("sc")) ? NULL : $this->postParam("sc"))) if(!$user->setShortCode(empty($this->postParam("sc")) ? NULL : $this->postParam("sc")))
$this->flashFail("err", tr("error"), tr("error_shorturl_incorrect")); $this->flashFail("err", tr("error"), tr("error_shorturl_incorrect"));
@ -400,11 +443,7 @@ final class UserPresenter extends OpenVKPresenter
throw $ex; throw $ex;
} }
$this->flash( $this->flash("succ", tr("changes_saved"), tr("changes_saved_comment"));
"succ",
"Изменения сохранены",
"Новые данные появятся на вашей странице."
);
} }
$this->template->mode = in_array($this->queryParam("act"), [ $this->template->mode = in_array($this->queryParam("act"), [
"main", "privacy", "finance", "finance.top-up", "interface" "main", "privacy", "finance", "finance.top-up", "interface"
@ -502,6 +541,9 @@ final class UserPresenter extends OpenVKPresenter
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"])
$this->flashFail("err", tr("error"), tr("feature_disabled"));
$receiverAddress = $this->postParam("receiver"); $receiverAddress = $this->postParam("receiver");
$value = (int) $this->postParam("value"); $value = (int) $this->postParam("value");
$message = $this->postParam("message"); $message = $this->postParam("message");
@ -517,7 +559,7 @@ final class UserPresenter extends OpenVKPresenter
$receiver = $this->users->getByAddress($receiverAddress); $receiver = $this->users->getByAddress($receiverAddress);
if(!$receiver) if(!$receiver)
$this->flashFail("err", tr("failed_to_tranfer_points"), tr("receiver_not_found")); $this->flashFail("err", tr("failed_to_tranfer_points"), tr("receiver_not_found"));
if($this->user->identity->getCoins() < $value) if($this->user->identity->getCoins() < $value)
$this->flashFail("err", tr("failed_to_tranfer_points"), tr("you_dont_have_enough_points")); $this->flashFail("err", tr("failed_to_tranfer_points"), tr("you_dont_have_enough_points"));
@ -574,4 +616,24 @@ final class UserPresenter extends OpenVKPresenter
$this->flashFail("succ", tr("information_-1"), tr("rating_increase_successful", $receiver->getURL(), htmlentities($receiver->getCanonicalName()), $value)); $this->flashFail("succ", tr("information_-1"), tr("rating_increase_successful", $receiver->getURL(), htmlentities($receiver->getCanonicalName()), $value));
} }
function renderEmailChangeFinish(): void
{
$request = (new EmailChangeVerifications)->getByToken(str_replace(" ", "+", $this->queryParam("key")));
if(!$request || !$request->isStillValid()) {
$this->flash("err", tr("token_manipulation_error"), tr("token_manipulation_error_comment"));
$this->redirect("/settings");
} else {
$request->delete(false);
try {
$request->getUser()->setEmail($request->getNewEmail());
} catch(UniqueConstraintViolationException $ex) {
$this->flashFail("err", tr("error"), tr("user_already_exists"));
}
$this->flash("succ", tr("changes_saved"), tr("changes_saved_comment"));
$this->redirect("/settings");
}
}
} }

View file

@ -64,7 +64,7 @@
<span class="nobold">{_"2fa_code"}</span> <span class="nobold">{_"2fa_code"}</span>
</td> </td>
<td> <td>
<input type="text" name="code" style="width: 100%;" /> <input type="text" name="password_change_code" style="width: 100%;" />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -154,6 +154,38 @@
{$user->getEmail()} {$user->getEmail()}
</td> </td>
</tr> </tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_new_email_address}</span>
</td>
<td>
<input type="email" name="new_email" style="width: 100%;" />
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_password}</span>
</td>
<td>
<input type="password" name="email_change_pass" style="width: 100%;" />
</td>
</tr>
<tr n:if="$user->is2faEnabled()">
<td width="120" valign="top">
<span class="nobold">{_"2fa_code"}</span>
</td>
<td>
<input type="text" name="email_change_code" style="width: 100%;" />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="submit" value="{_save_email_address}" class="button" />
</td>
</tr>
</tbody> </tbody>
</table> </table>
<br/> <br/>

View file

@ -73,6 +73,8 @@ routes:
handler: "User->disableTwoFactorAuth" handler: "User->disableTwoFactorAuth"
- url: "/settings/reset_theme" - url: "/settings/reset_theme"
handler: "User->resetThemepack" handler: "User->resetThemepack"
- url: "/settings/change_email"
handler: "User->emailChangeFinish"
- url: "/coins_transfer" - url: "/coins_transfer"
handler: "User->coinsTransfer" handler: "User->coinsTransfer"
- url: "/increase_social_credits" - url: "/increase_social_credits"

View file

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS `email_change_verifications` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`profile` bigint(20) unsigned NOT NULL,
`key` char(64) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`new_email` varchar(90) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
`timestamp` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

View file

@ -444,6 +444,8 @@
"your_page_address" = "Your address page"; "your_page_address" = "Your address page";
"page_address" = "Address page"; "page_address" = "Address page";
"current_email_address" = "Current email address"; "current_email_address" = "Current email address";
"new_email_address" = "New email address";
"save_email_address" = "Save email address";
"page_id" = "Page ID"; "page_id" = "Page ID";
"you_can_also" = "You can also"; "you_can_also" = "You can also";
"delete_your_page" = "delete your page"; "delete_your_page" = "delete your page";
@ -458,6 +460,8 @@
"additional_links" = "Additional links"; "additional_links" = "Additional links";
"ad_poster" = "Ad poster"; "ad_poster" = "Ad poster";
"email_change_confirm_message" = "Please confirm your new email address for the change to take effect. We have sent instructions to it.";
/* Two-factor authentication */ /* Two-factor authentication */
"two_factor_authentication" = "Two-factor authentication"; "two_factor_authentication" = "Two-factor authentication";

View file

@ -472,6 +472,8 @@
"your_page_address" = "Адрес Вашей страницы"; "your_page_address" = "Адрес Вашей страницы";
"page_address" = "Адрес страницы"; "page_address" = "Адрес страницы";
"current_email_address" = "Текущий адрес"; "current_email_address" = "Текущий адрес";
"new_email_address" = "Новый адрес";
"save_email_address" = "Сохранить адрес";
"page_id" = "ID страницы"; "page_id" = "ID страницы";
"you_can_also" = "Вы также можете"; "you_can_also" = "Вы также можете";
"delete_your_page" = "удалить свою страницу"; "delete_your_page" = "удалить свою страницу";
@ -486,6 +488,8 @@
"additional_links" = "Дополнительные ссылки"; "additional_links" = "Дополнительные ссылки";
"ad_poster" = "Рекламный плакат"; "ad_poster" = "Рекламный плакат";
"email_change_confirm_message" = "Чтобы изменение вступило в силу, подтвердите ваш новый адрес электронной почты. Мы отправили инструкции на него.";
/* Two-factor authentication */ /* Two-factor authentication */
"two_factor_authentication" = "Двухфакторная аутентификация"; "two_factor_authentication" = "Двухфакторная аутентификация";