diff --git a/endpoints/account/activate.php b/endpoints/account/activate.php new file mode 100644 index 00000000..d437d75c --- /dev/null +++ b/endpoints/account/activate.php @@ -0,0 +1,73 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + public function __construct() + { + parent::__construct(); + + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + $msg = $this->activate(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'register', [2]), 'message' => $msg]]; + else + { + $_SESSION['error']['activate'] = $msg; + $this->forward('?account=resend'); + } + + parent::generate(); + } + + private function activate() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + if (DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `status` IN (?a) AND `token` = ?', [ACC_STATUS_NONE, ACC_STATUS_NEW], $this->_get['key'])) + { + // don't remove the token yet. It's needed on signin page. + DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `userGroups` = ?d WHERE `token` = ?', ACC_STATUS_NONE, U_GROUP_NONE, $this->_get['key']); + + // fully apply block for further registration attempts from this ip + DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d + 1, UNIX_TIMESTAMP() + ?d)', + User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + $this->success = true; + return Lang::account('inputbox', 'message', 'accActivated', [$this->_get['key']]); + } + + // grace period expired and other user claimed name + return Lang::main('intError'); + } +} + +?> diff --git a/endpoints/account/signin.php b/endpoints/account/signin.php index dce60520..c4a0329e 100644 --- a/endpoints/account/signin.php +++ b/endpoints/account/signin.php @@ -19,6 +19,7 @@ class AccountSigninResponse extends TemplateResponse use TrGetNext; protected string $template = 'text-page-generic'; + protected string $pageName = 'signin'; protected array $expectedPOST = array( 'username' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateLogin'] ], @@ -26,8 +27,8 @@ class AccountSigninResponse extends TemplateResponse 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe'] ] ); protected array $expectedGET = array( - 'token' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{32}$/']], - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] ); private bool $success = false; @@ -43,46 +44,54 @@ class AccountSigninResponse extends TemplateResponse protected function generate() : void { - $username = ''; - $message = ''; + $username = + $error = ''; + $rememberMe = !!$this->_post['remember_me']; $this->title = [Lang::account('title')]; - if ($this->_get['token']) + // coming from user recovery or creation, prefill username + if ($this->_get['key']) { - // coming from username recovery, prefill username - if ($_ = DB::Aowow()->selectCell('SELECT `login` FROM ?_account WHERE `status` IN (?a) AND `token` = ? AND `statusTimer` > UNIX_TIMESTAMP()', [ACC_STATUS_RECOVER_USER, ACC_STATUS_OK], $this->_get['token'])) - $username = $_; + if ($userData = DB::Aowow()->selectRow('SELECT a.`login` AS "0", IF(s.`expires`, 0, 1) AS "1" FROM ?_account a LEFT JOIN ?_account_sessions s ON a.`id` = s.`userId` AND a.`token` = s.`sessionId` WHERE a.`status` IN (?a) AND a.`token` = ?', + [ACC_STATUS_RECOVER_USER, ACC_STATUS_NONE], $this->_get['key'])) + [$username, $rememberMe] = $userData; } - $message = $this->doSignIn(); - if (!$this->success) - User::destroy(); - else + if ($this->doSignIn($error)) $this->forward($this->getNext(true)); + if ($error) + User::destroy(); + $this->inputbox = ['inputbox-form-signin', array( 'head' => Lang::account('inputbox', 'head', 'signin'), 'action' => '?account=signin&next='.$this->getNext(), - 'error' => $message, + 'error' => $error, 'username' => $username, - 'rememberMe' => !!$this->_post['remember_me'], + 'rememberMe' => $rememberMe, 'hasRecovery' => Cfg::get('ACC_EXT_RECOVER_URL') || Cfg::get('ACC_AUTH_MODE') == AUTH_MODE_SELF, )]; parent::generate(); } - private function doSignIn() : string + private function doSignIn(string &$error) : bool { if (is_null($this->_post['username']) && is_null($this->_post['password'])) - return ''; + return false; if (!$this->assertPOST('username')) - return Lang::account('userNotFound'); + { + $error = Lang::account('userNotFound'); + return false; + } if (!$this->assertPOST('password')) - return Lang::account('wrongPass'); + { + $error = Lang::account('wrongPass'); + return false; + } $error = match (User::authenticate($this->_post['username'], $this->_post['password'])) { @@ -95,10 +104,7 @@ class AccountSigninResponse extends TemplateResponse default => Lang::main('intError') }; - if (!$error) - $this->success = true; - - return $error; + return !$error; } private function onAuthSuccess() : string @@ -109,14 +115,11 @@ class AccountSigninResponse extends TemplateResponse return Lang::main('intError'); } - $email = filter_var($this->_post['username'], FILTER_VALIDATE_EMAIL); - // reset account status, update expiration - $ok = DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE { `email` = ? } { `login` = ? }', + $ok = DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE `id` = ?d', User::$ip, User::$ip, User::$ip, ACC_STATUS_NEW, ACC_STATUS_NEW, ACC_STATUS_NEW, - $email ?: DBSIMPLE_SKIP, - !$email ? $this->_post['username'] : DBSIMPLE_SKIP + User::$id // available after successful User:authenticate ); if (!is_int($ok)) // num updated fields or null on fail @@ -125,6 +128,10 @@ class AccountSigninResponse extends TemplateResponse return Lang::main('intError'); } + // DELETE temp session + if ($this->_get['key']) + DB::Aowow()->query('DELETE FROM ?_account_sessions WHERE `sessionId` = ?', $this->_get['key']); + session_regenerate_id(true); // user status changed => regenerate id // create new session entry diff --git a/endpoints/account/signup.php b/endpoints/account/signup.php new file mode 100644 index 00000000..f8a819e1 --- /dev/null +++ b/endpoints/account/signup.php @@ -0,0 +1,163 @@ + ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'email' => ['filter' => FILTER_SANITIZE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe']] + ); + + protected array $expectedGET = array( + 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct() + { + // if the user is logged in goto account dashboard + if (User::isLoggedIn()) + $this->forward('?account'); + + // redirect to external registration page, if set + if (Cfg::get('ACC_EXT_CREATE_URL')) + $this->forward(Cfg::get('ACC_EXT_CREATE_URL')); + + parent::__construct(); + + // registration not enabled on self + if (!Cfg::get('ACC_ALLOW_REGISTER')) + $this->generateError(); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + // step 1 - no params > signup form + // step 2 - any param > status box + // step 3 - on ?account=activate + + $message = $this->doSignUp(); + + if ($this->success) + { + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1.5]), + 'message' => Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]) + )]; + } + else + { + $this->inputbox = ['inputbox-form-signup', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1]), + 'error' => $message, + 'action' => '?account=signup&next='.$this->getNext(), + 'username' => $this->_post['username'] ?? '', + 'email' => $this->_post['email'] ?? '', + 'rememberMe' => !!$this->_post['remember_me'], + )]; + } + + parent::generate(); + } + + private function doSignUp() : string + { + // no input yet. show clean form + if (!$this->assertPOST('username', 'password', 'c_password') && is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + // check username + if (!Util::validateUsername($this->_post['username'], $e)) + return Lang::account($e == 1 ? 'errNameLength' : 'errNameChars'); + + // check password + if (!Util::validatePassword($this->_post['password'], $e)) + return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'); + + if ($this->_post['password'] !== $this->_post['c_password']) + return Lang::account('passMismatch'); + + // check ip + if (!User::$ip) + return Lang::main('intError'); + + // limit account creation + if (DB::Aowow()->selectRow('SELECT 1 FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ? AND `count` >= ?d AND `unbanDate` >= UNIX_TIMESTAMP()', IP_BAN_TYPE_REGISTRATION_ATTEMPT, User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT'))) + { + DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = ?d', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT); + return Lang::account('inputbox', 'error', 'signupExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]); + } + + // username / email taken + if ($inUseData = DB::Aowow()->SelectRow('SELECT `id`, `username`, `status` = ?d AND `statusTimer` < UNIX_TIMESTAMP() AS "expired" FROM ?_account WHERE (LOWER(`username`) = LOWER(?) OR LOWER(`email`) = LOWER(?))', ACC_STATUS_NEW, $this->_post['username'], $this->_post['email'])) + { + if ($inUseData['expired']) + DB::Aowow()->query('DELETE FROM ?_account WHERE `id` = ?d', $inUseData['id']); + else + return Util::lower($inUseData['username']) == Util::lower($this->_post['username']) ? Lang::account('nameInUse') : Lang::account('mailInUse'); + } + + // create.. + $token = Util::createHash(); + $userId = DB::Aowow()->query('INSERT INTO ?_account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)', + $this->_post['username'], + User::hashCrypt($this->_post['password']), + $this->_post['username'], + $this->_post['email'], + User::$ip, + Lang::getLocale()->value, + U_GROUP_PENDING, + ACC_STATUS_NEW, + Cfg::get('ACC_CREATE_SAVE_DECAY'), + $token + ); + + if (!$userId) + return Lang::main('intError'); + + // create session tied to the token to store remember_me status + DB::Aowow()->query('INSERT INTO ?_account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (?d, ?, ?d, ?d, ?d, ?, ?, ?d)', + $userId, $token, time(), $this->_post['remember_me'] ? 0 : time() + Cfg::get('SESSION_TIMEOUT_DELAY'), time(), User::$agent, User::$ip, SESSION_ACTIVE); + + if (!Util::sendMail($this->_post['email'], 'activate-account', [$token], Cfg::get('ACC_CREATE_SAVE_DECAY'))) + return Lang::main('intError2', ['send mail']); + + // success: update ip-bans + DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, 1, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d', + User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + Util::gainSiteReputation($userId, SITEREP_ACTION_REGISTER); + + $this->success = true; + return ''; + } +} + +?> diff --git a/template/pages/acc-signUp.tpl.php b/template/bricks/inputbox-form-signup.tpl.php similarity index 79% rename from template/pages/acc-signUp.tpl.php rename to template/bricks/inputbox-form-signup.tpl.php index 6b439795..20724f66 100644 --- a/template/pages/acc-signUp.tpl.php +++ b/template/bricks/inputbox-form-signup.tpl.php @@ -1,23 +1,10 @@ - - -brick('header'); ?> - -