Template/Update (Part 46 - II)

* account management rework: Signup functionality
This commit is contained in:
Sarjuuk
2025-08-28 17:43:07 +02:00
parent 155bf1e4a3
commit f16479b50c
4 changed files with 279 additions and 57 deletions

View File

@@ -0,0 +1,73 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via activation email link
* empty page with status box
*/
class AccountActivateResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'activate';
protected array $expectedGET = array(
'key' => ['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');
}
}
?>

View File

@@ -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

View File

@@ -0,0 +1,163 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via signup link
* self referencing
*/
class AccountSignupResponse extends TemplateResponse
{
use TrGetNext;
protected string $template = 'text-page-generic';
protected string $pageName = 'signup';
protected array $expectedPOST = array(
'username' => ['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 '';
}
}
?>