Template/Update (Part 46 - IV)

* account management rework: Personal Settings functionality
 * email, password, username update
 * email updates now also mails the old address for confirmation
This commit is contained in:
Sarjuuk
2025-08-28 17:55:08 +02:00
parent 8fadce88ad
commit 258ac19f0a
36 changed files with 628 additions and 15 deletions

View File

@@ -28,6 +28,7 @@ class AccountBaseResponse extends TemplateResponse
public string $curEmail = '';
public string $curName = '';
public string $renameCD = '';
public string $activeCD = '';
public array $description = [];
public array $signature = [];
public int $avMode = 0;
@@ -51,7 +52,7 @@ class AccountBaseResponse extends TemplateResponse
{
array_unshift($this->title, Lang::account('settings'));
$user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon` FROM ?_account WHERE `id` = ?d', User::$id);
$user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon`, `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id);
Lang::sort('game', 'ra');
@@ -108,10 +109,13 @@ class AccountBaseResponse extends TemplateResponse
$this->curEmail = $user['email'] ?? '';
// Username
$this->curName = User::$username;
// todo localize date format; store time
// $this->renameCD = date('F j, o', time() + 7 * DAY);
$this->curName = User::$username;
$this->renameCD = Util::formatTime(Cfg::get('ACC_RENAME_DECAY') * 1000);
if ($user['renameCooldown'] > time())
{
$locCode = implode('_', str_split(Lang::getLocale()->json(), 2)); // ._.
$this->activeCD = (new \IntlDateFormatter($locCode, pattern: Lang::main('dateFmtIntl')))->format($user['renameCooldown']);
}
/* COMMUNITY */

View File

@@ -0,0 +1,62 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via confirmation email link
* write status to session and redirect to account settings
*/
// ?auth=email-change
class AccountConfirmemailaddressResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'confirm-email-address';
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']]
);
private bool $success = false;
protected function generate() : void
{
parent::generate();
if (User::isBanned())
return;
$msg = $this->change();
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'),
'message' => $this->success ? $msg : '',
'error' => $this->success ? '' : $msg,
)];
}
// this should probably leave change info intact for revert
// todo - move personal settings changes to separate table
private function change() : string
{
if (!$this->assertGET('key'))
return Lang::main('intError');
$acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']);
if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time())
return Lang::account('inputbox', 'error', 'mailTokenUsed');
// 0 changes == error
if (!DB::Aowow()->query('UPDATE ?_account SET `email` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key']))
return Lang::main('intError');
$this->success = true;
return Lang::account('inputbox', 'message', 'mailChangeOk');
}
}
?>

View File

@@ -0,0 +1,60 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via confirmation email link
* write status to session and redirect to account settings
*/
// 2025 - no longer in use?
class AccountConfirmpasswordResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'confirm-password';
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']]
);
private bool $success = false;
protected function generate() : void
{
parent::generate();
if (User::isBanned())
return;
$msg = $this->confirm();
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'),
'message' => $this->success ? $msg : '',
'error' => $this->success ? '' : $msg,
)];
}
private function confirm() : string
{
if (!$this->assertGET('key'))
return Lang::main('intError');
$acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']);
if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_PASS || $acc['statusTimer'] < time())
return Lang::account('inputbox', 'error', 'passTokenUsed');
// 0 changes == error
if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key']))
return Lang::main('intError');
$this->success = true;
return Lang::account('inputbox', 'message', 'passChangeOk');
}
}
?>

View File

@@ -0,0 +1,62 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via revert email link
* write status to session and redirect to account settings
*/
// ?auth=email-revert
class AccountRevertemailaddressResponse extends TemplateResponse
{
protected string $template = 'text-page-generic';
protected string $pageName = 'revert-email-address';
protected array $expectedGET = array(
'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']]
);
private bool $success = false;
protected function generate() : void
{
parent::generate();
if (User::isBanned())
return;
$msg = $this->revert();
$this->inputbox = ['inputbox-status', array(
'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'),
'message' => $this->success ? $msg : '',
'error' => $this->success ? '' : $msg,
)];
}
// this should probably take precedence over email-change
// todo - move personal settings changes to separate table
private function revert() : string
{
if (!$this->assertGET('key'))
return Lang::main('intError');
$acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']);
if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time())
return Lang::account('inputbox', 'error', 'mailTokenUsed');
// 0 changes == error
if (!DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key']))
return Lang::main('intError');
$this->success = true;
return Lang::account('inputbox', 'message', 'mailRevertOk');
}
}
?>

View File

@@ -0,0 +1,80 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via account settings form submit
* write status to session and redirect to account settings
*/
class AccountUpdateemailResponse extends TextResponse
{
protected ?string $redirectTo = '?account#personal';
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'newemail' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW]
);
private bool $success = false;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
(new TemplateResponse())->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
if (User::isBanned())
return;
if ($msg = $this->updateMail())
$_SESSION['msg'] = ['email', $this->success, $msg];
}
private function updateMail() : string
{
// no input yet
if (is_null($this->_post['newemail']))
return Lang::main('intError');
// truncated due to validation fail
if (!$this->_post['newemail'])
return Lang::account('emailInvalid');
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ? AND `id` <> ?d', $this->_post['newemail'], User::$id))
return Lang::account('mailInUse');
$status = DB::Aowow()->selectCell('SELECT `status` FROM ?_account WHERE `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', User::$id);
if ($status != ACC_STATUS_NONE && $status != ACC_STATUS_CHANGE_EMAIL)
return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]);
$oldEmail = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id);
if ($this->_post['newemail'] == $oldEmail)
return Lang::account('newMailDiff');
$token = Util::createHash();
// store new mail in updateValue field, exchange when confirmation mail gets confirmed
if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d',
$this->_post['newemail'], ACC_STATUS_CHANGE_EMAIL, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id))
return Lang::main('intError');
if (!Util::sendMail($this->_post['newemail'], 'change-email', [$token, $this->_post['newemail']], Cfg::get('ACC_RECOVERY_DECAY')))
return Lang::main('intError2', ['send mail']);
if (!Util::sendMail($oldEmail, 'revert-email', [$token, $oldEmail], Cfg::get('ACC_RECOVERY_DECAY')))
return Lang::main('intError2', ['send mail']);
$this->success = true;
return Lang::account('updateMessage', 'personal', [$this->_post['newemail']]);
}
}
?>

View File

@@ -0,0 +1,86 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via account settings form submit
* write status to session and redirect to account settings
*/
class AccountUpdatepasswordResponse extends TextResponse
{
protected ?string $redirectTo = '?account#personal';
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'currentPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']],
'newPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']],
'confirmPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']],
'globalLogout' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCheckbox']]
);
private bool $success = false;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
(new TemplateResponse())->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
if (User::isBanned())
return;
if ($msg = $this->updatePassword())
$_SESSION['msg'] = ['password', $this->success, $msg];
}
private function updatePassword() : string
{
if (!$this->assertPOST('currentPassword', 'newPassword', 'confirmPassword'))
return Lang::main('intError');
if (!Util::validatePassword($this->_post['newPassword'], $e))
return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars');
if ($this->_post['newPassword'] !== $this->_post['confirmPassword'])
return Lang::account('passMismatch');
$userData = DB::Aowow()->selectRow('SELECT `status`, `passHash`, `statusTimer` FROM ?_account WHERE `id` = ?d', User::$id);
if ($userData['status'] != ACC_STATUS_NONE && $userData['status'] != ACC_STATUS_CHANGE_PASS && $userData['statusTimer'] > time())
return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]);
if (!User::verifyCrypt($this->_post['currentPassword'], $userData['passHash']))
return Lang::account('wrongPass');
if (User::verifyCrypt($this->_post['newPassword'], $userData['passHash']))
return Lang::account('newPassDiff');
$token = Util::createHash();
// store new hash in updateValue field, exchange when confirmation mail gets confirmed
if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d',
User::hashCrypt($this->_post['newPassword']), ACC_STATUS_CHANGE_PASS, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id))
return Lang::main('intError');
$email = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id);
if (!Util::sendMail($email, 'update-password', [$token, $email], Cfg::get('ACC_RECOVERY_DECAY')))
return Lang::main('intError2', ['send mail']);
// logout all other active sessions
if ($this->_post['globalLogout'])
DB::Aowow()->query('UPDATE ?_account_sessions SET `status` = ?d, `touched` = ?d WHERE `userId` = ?d AND `sessionId` <> ? AND `status` = ?d', SESSION_FORCED_LOGOUT, time(), User::$id, session_id(), SESSION_ACTIVE);
$this->success = true;
return Lang::account('updateMessage', 'personal', [User::$email]);
}
}
?>

View File

@@ -0,0 +1,61 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/*
* accessed via account settings form submit
* write status to session and redirect to account settings
*/
class AccountUpdateusernameResponse extends TextResponse
{
protected ?string $redirectTo = '?account#personal';
protected bool $requiresLogin = true;
protected array $expectedPOST = array(
'newUsername' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']]
);
private bool $success = false;
public function __construct(string $pageParam)
{
if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF)
(new TemplateResponse())->generateError();
parent::__construct($pageParam);
}
protected function generate() : void
{
if (User::isBanned())
return;
if ($msg = $this->updateUsername())
$_SESSION['msg'] = ['username', $this->success, $msg];
}
private function updateUsername() : string
{
if (!$this->assertPOST('newUsername'))
return Lang::main('intError');
if (DB::Aowow()->selectCell('SELECT `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id) > time())
return Lang::main('intError'); // should have grabbed the error response..
// yes, including your current name. you don't want to change into your current name, right?
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_post['newUsername']))
return Lang::account('nameInUse');
DB::Aowow()->query('UPDATE ?_account SET `username` = ?, `renameCooldown` = ?d WHERE `id` = ?d', $this->_post['newUsername'], time() + Cfg::get('acc_rename_decay'), User::$id);
$this->success = true;
return Lang::account('updateMessage', 'username', [User::$username, $this->_post['newUsername']]);
}
}
?>