diff --git a/endpoints/account/forgot-password.php b/endpoints/account/forgot-password.php new file mode 100644 index 00000000..4121f47f --- /dev/null +++ b/endpoints/account/forgot-password.php @@ -0,0 +1,101 @@ + 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']]); + } +} + +?> diff --git a/endpoints/account/forgot-username.php b/endpoints/account/forgot-username.php new file mode 100644 index 00000000..4a6245d4 --- /dev/null +++ b/endpoints/account/forgot-username.php @@ -0,0 +1,100 @@ + 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']]); + } +} + +?> diff --git a/endpoints/account/resend-submit.php b/endpoints/account/resend-submit.php new file mode 100644 index 00000000..c45c0499 --- /dev/null +++ b/endpoints/account/resend-submit.php @@ -0,0 +1,52 @@ + ['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 + )]; + } +} + +?> diff --git a/endpoints/account/resend.php b/endpoints/account/resend.php new file mode 100644 index 00000000..234aea17 --- /dev/null +++ b/endpoints/account/resend.php @@ -0,0 +1,98 @@ + ['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']]); + } +} + +?> diff --git a/endpoints/account/reset-password.php b/endpoints/account/reset-password.php new file mode 100644 index 00000000..44c39b0b --- /dev/null +++ b/endpoints/account/reset-password.php @@ -0,0 +1,121 @@ + 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 ''; + } +} + +?> diff --git a/template/bricks/inputbox-form-email.tpl.php b/template/bricks/inputbox-form-email.tpl.php new file mode 100644 index 00000000..dfc7be5d --- /dev/null +++ b/template/bricks/inputbox-form-email.tpl.php @@ -0,0 +1,46 @@ + +
+ + + + + diff --git a/template/bricks/inputbox-form-password.tpl.php b/template/bricks/inputbox-form-password.tpl.php new file mode 100644 index 00000000..5f76c375 --- /dev/null +++ b/template/bricks/inputbox-form-password.tpl.php @@ -0,0 +1,78 @@ + + + + + + + + diff --git a/template/bricks/inputbox-form-signin.tpl.php b/template/bricks/inputbox-form-signin.tpl.php index a917c7a5..4fa30d83 100644 --- a/template/bricks/inputbox-form-signin.tpl.php +++ b/template/bricks/inputbox-form-signin.tpl.php @@ -52,7 +52,7 @@