Template/Update (Part 46 - III)

* account management rework: Recovery Options
This commit is contained in:
Sarjuuk
2025-08-28 17:51:36 +02:00
parent f16479b50c
commit 8fadce88ad
15 changed files with 603 additions and 143 deletions

View File

@@ -0,0 +1,101 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via links on signin form and from recovery email
*
* A) redirect to external page
* B) 1. click password reset link > display email form
* 2. submit email form > send mail with recovery link
* 3. click recovery link from mail > display password reset form
* 4. submit password reset form > update password
*/
class AccountforgotpasswordResponse extends TemplateResponse
{
use TrRecoveryHelper, TrGetNext;
protected string $template = 'text-page-generic';
protected string $pageName = 'forgot-password';
protected array $expectedPOST = array(
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
private bool $success = false;
public function __construct(string $pageParam)
{
// don't redirect logged in users
// you can be forgetful AND logged in
if (Cfg::get('ACC_EXT_RECOVER_URL'))
$this->forward(Cfg::get('ACC_EXT_RECOVER_URL'));
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
parent::generate();
$msg = $this->processMailForm();
if ($this->success)
$this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverPass', [1.5]), 'message' => $msg]];
else
$this->inputbox = ['inputbox-form-email', array(
'head' => Lang::account('inputbox', 'head', 'recoverPass', [1]),
'error' => $msg,
'action' => '?account=forgot-password&next='.$this->getNext(),
'email' => $this->_post['email'] ?? ''
)];
}
private function processMailForm() : string
{
// no input yet. show clean email form
if (is_null($this->_post['email']))
return '';
// truncated due to validation fail
if (!$this->_post['email'])
return Lang::account('emailInvalid');
$timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT'));
// on cooldown pretend we dont know the email address
if ($timeout && $timeout > time())
return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound');
// pretend recovery started
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email']))
{
// do not confirm or deny existence of email
$this->success = !Cfg::get('DEBUG');
return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]);
}
// recovery actually started
if ($err = $this->startRecovery(ACC_STATUS_RECOVER_PASS, 'reset-password', $this->_post['email']))
return $err;
DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d',
User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK'));
$this->success = true;
return Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]);
}
}
?>

View File

@@ -0,0 +1,100 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via link on signin form
*
* A) redirect to external page
* B) 1. click password reset link > display email form
* 2. submit email form > send mail with recovery link
* ( 3. click recovery link from mail to go to signin page (so not on this page) )
*/
class AccountforgotusernameResponse extends TemplateResponse
{
use TrRecoveryHelper;
protected string $template = 'text-page-generic';
protected string $pageName = 'forgot-username';
protected array $expectedPOST = array(
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
private bool $success = false;
public function __construct(string $pageParam)
{
// if the user is looged in goto account dashboard
if (User::isLoggedIn())
$this->forward('?account');
if (Cfg::get('ACC_EXT_RECOVER_URL'))
$this->forward(Cfg::get('ACC_EXT_RECOVER_URL'));
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
parent::generate();
$msg = $this->processMailForm();
if ($this->success)
$this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverUser'), 'message' => $msg]];
else
$this->inputbox = ['inputbox-form-email', array(
'head' => Lang::account('inputbox', 'head', 'recoverUser'),
'error' => $msg,
'action' => '?account=forgot-username'
)];
}
private function processMailForm() : string
{
// no input yet. show empty form
if (is_null($this->_post['email']))
return '';
// truncated due to validation fail
if (!$this->_post['email'])
return Lang::account('emailInvalid');
$timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT'));
// on cooldown pretend we dont know the email address
if ($timeout && $timeout > time())
return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound');
// pretend recovery started
if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email']))
{
// do not confirm or deny existence of email
$this->success = !Cfg::get('DEBUG');
return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]);
}
// recovery actually started
if ($err = $this->startRecovery(ACC_STATUS_RECOVER_USER, 'recover-user', $this->_post['email']))
return $err;
DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d',
User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK'));
$this->success = true;
return Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]);
}
}
?>

View File

@@ -0,0 +1,52 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed after successful resend request
* empty page with status box
*/
class AccountResendsubmitResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'resend';
protected array $expectedPOST = array(
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
public function __construct(string $pageParam)
{
if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
$error = $message = '';
if ($this->assertPOST('email'))
$message = Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]);
else
$error = Lang::main('intError');
parent::generate();
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', 'register', [1.5]),
'message' => $message,
'error' => $error
)];
}
}
?>

View File

@@ -0,0 +1,98 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via link on login page
* empty page with status box
*/
class AccountResendResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'resend';
protected array $expectedPOST = array(
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
private bool $success = false;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_EXT_RECOVER_URL'))
$this->forward(Cfg::get('ACC_EXT_RECOVER_URL'));
if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
$this->title[] = Lang::account('title');
parent::generate();
// error from account=activate
if (isset($_SESSION['error']['activate']))
{
$msg = $_SESSION['error']['activate'];
unset($_SESSION['error']['activate']);
}
else
$msg = $this->resend();
if ($this->success)
$this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'resendMail'), 'message' => $msg]];
else
$this->inputbox = ['inputbox-form-email', array(
'head' => Lang::account('inputbox', 'head', 'resendMail'),
'message' => Lang::account('inputbox', 'message', 'resendMail'),
'error' => $msg,
'action' => '?account=resend',
)];
}
private function resend() : string
{
// no input yet. show clean form
if (is_null($this->_post['email']))
return '';
// truncated due to validation fail
if (!$this->_post['email'])
return Lang::account('emailInvalid');
$timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT'));
// on cooldown pretend we dont know the email address
if ($timeout && $timeout > time())
return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound');
// check email and account status
if ($token = DB::Aowow()->selectCell('SELECT `token` FROM ?_account WHERE `email` = ? AND `status` = ?d', $this->_post['email'], ACC_STATUS_NEW))
{
if (!Util::sendMail($this->_post['email'], 'activate-account', [$token]))
return Lang::main('intError');
DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d',
User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK'));
$this->success = true;
return Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]);
}
// pretend recovery started
// do not confirm or deny existence of email
$this->success = !Cfg::get('DEBUG');
return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]);
}
}
?>

View File

@@ -0,0 +1,121 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via links on signin form and from recovery email
*
* A) redirect to external page
* B) 1. click password reset link > display email form
* 2. submit email form > send mail with recovery link
* 3. click recovery link from mail > display password reset form
* 4. submit password reset form > update password
*/
class AccountresetpasswordResponse extends TemplateResponse
{
use TrRecoveryHelper, TrGetNext;
protected string $template = 'text-page-generic';
protected string $pageName = 'reset-password';
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']],
'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ]
);
protected array $expectedPOST = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']],
'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ],
'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ],
'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ]
);
private bool $success = false;
public function __construct()
{
$this->title[] = Lang::account('title');
parent::__construct();
// don't redirect logged in users
// you can be forgetful AND logged in
if (Cfg::get('ACC_EXT_RECOVER_URL'))
$this->forward(Cfg::get('ACC_EXT_RECOVER_URL'));
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
$this->generateError();
}
protected function generate() : void
{
parent::generate();
$errMsg = '';
if (!$this->assertGET('key') && !$this->assertPOST('key'))
$errMsg = Lang::account('inputbox', 'error', 'passTokenLost');
else if ($this->_get['key'] && !DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `token` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', $this->_get['key'], ACC_STATUS_RECOVER_PASS))
$errMsg = Lang::account('inputbox', 'error', 'passTokenUsed');
if ($errMsg)
{
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', 'error'),
'error' => $errMsg
)];
return;
}
// step "2.5"
$errMsg = $this->doResetPass();
if ($this->success)
$this->forward('?account=signin');
// step 2
$this->inputbox = ['inputbox-form-password', array(
'head' => Lang::account('inputbox', 'head', 'recoverPass', [2]),
'token' => $this->_post['key'] ?? $this->_get['key'],
'action' => '?account=reset-password&next=account=signin',
'error' => $errMsg,
)];
}
private function doResetPass() : string
{
// no input yet. show clean form
if (!$this->assertPOST('key', 'password', 'c_password') && is_null($this->_post['email']))
return '';
// truncated due to validation fail
if (!$this->_post['email'])
return Lang::account('emailInvalid');
if ($this->_post['password'] != $this->_post['c_password'])
return Lang::account('passCheckFail');
$userData = DB::Aowow()->selectRow('SELECT `id`, `passHash` FROM ?_account WHERE `token` = ? AND `email` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()',
$this->_post['key'],
$this->_post['email'],
ACC_STATUS_RECOVER_PASS
);
if (!$userData)
return Lang::account('inputbox', 'error', 'emailNotFound');
if (!User::verifyCrypt($this->_post['c_password'], $userData['passHash']))
return Lang::account('newPassDiff');
if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = ?, `status` = ?d WHERE `id` = ?d', User::hashCrypt($this->_post['c_password']), ACC_STATUS_NONE, $userData['id']))
return Lang::main('intError');
$this->success = true;
return '';
}
}
?>