OAuth refactoring

The registration functionality has been moved from RegLog to the Register page.
Added a check for agreeing to the rules for the first login.
Added ability to edit name when registering via OAuth.
This commit is contained in:
Visman 2023-06-01 23:00:59 +07:00
parent d50c4c81d9
commit 604e0d2a13
5 changed files with 289 additions and 176 deletions

View file

@ -12,12 +12,15 @@ namespace ForkBB\Models\Pages;
use ForkBB\Core\Image;
use ForkBB\Models\Page;
use ForkBB\Models\Pages\RegLogTrait;
use ForkBB\Models\Provider\Driver;
use ForkBB\Models\User\User;
use function \ForkBB\__;
class RegLog extends Page
{
use RegLogTrait;
const TIMEOUT = 5;
/**
@ -110,8 +113,8 @@ class RegLog extends Page
protected function byGuest(Driver $provider, int $uid): Page
{
switch ($provider->stateType) {
case 'reg':
case 'auth':
case 'reg':
return $this->authOrReg($provider, $uid);
default:
return $this->c->Message->message('Bad request');
@ -166,127 +169,42 @@ class RegLog extends Page
*/
protected function authOrReg(Driver $provider, int $uid): Page
{
// регистрация
if (empty($uid)) {
// на форуме есть пользователь с таким email
if (
$this->c->providerUser->findByEmail($provider->userEmail) > 0
|| $this->c->users->loadByEmail($provider->userEmail) instanceof User
) {
$auth = $this->c->Auth;
$auth->fIswev = [FORK_MESS_INFO, ['Email message', __($provider->name)]];
return $auth->forget([], 'GET', $provider->userEmail);
}
if (1 !== $this->c->config->b_regs_allow) {
return $this->c->Message->message('No new regs');
}
$user = $this->c->users->create();
$user->username = $this->nameGenerator($provider);
$user->password = 'oauth_' . $this->c->Secury->randomPass(7);
$user->group_id = $this->c->config->i_default_user_group;
$user->email = $provider->userEmail;
$user->email_confirmed = $provider->userEmailVerifed ? 1 : 0;
$user->activate_string = '';
$user->u_mark_all_read = \time();
$user->email_setting = $this->c->config->i_default_email_setting;
$user->timezone = $this->c->config->o_default_timezone;
$user->language = $this->user->language;
$user->style = $this->user->style;
$user->registered = \time();
$user->registration_ip = $this->user->ip;
$user->ip_check_type = 0;
$user->signature = '';
$user->location = $provider->userLocation;
$user->url = $provider->userURL;
if ($provider->userAvatar) {
$image = $this->c->Files->uploadFromLink($provider->userAvatar);
if ($image instanceof Image) {
$name = $this->c->Secury->randomPass(8);
$path = $this->c->DIR_PUBLIC . "{$this->c->config->o_avatars_dir}/{$name}.(webp|jpg|png|gif)";
$result = $image
->rename(true)
->rewrite(false)
->resize($this->c->config->i_avatars_width, $this->c->config->i_avatars_height)
->toFile($path, $this->c->config->i_avatars_size);
if (true === $result) {
$user->avatar = $image->name() . '.' . $image->ext();
} else {
$this->c->Log->warning('OAuth Failed image processing', [
'user' => $user->fLog(),
'error' => $image->error(),
]);
}
} else {
$this->c->Log->warning('OAuth Avatar not image', [
'user' => $user->fLog(),
'error' => $this->c->Files->error(),
]);
}
}
$this->c->users->insert($user);
if (true !== $this->c->providerUser->registration($user, $provider)) {
throw new RuntimeException('Failed to insert data'); // ??????????????????????????????????????????
}
$this->c->Log->info('OAuth Reg: ok', [
'user' => $user->fLog(),
'provider' => $provider->name,
'userInfo' => $provider->userInfo,
'headers' => true,
]);
} else {
$user = $this->c->users->load($uid);
}
// вход
return $this->c->Auth->login([], 'POST', '', $user);
}
if ($uid > 0) {
$user = $this->c->users->load($uid);
/**
* Подбирает уникальное имя для регистрации пользователя
*/
protected function nameGenerator(Driver $provider): string
{
$names = [];
if ('' != $provider->userName) {
$names[] = $provider->userName;
return $this->c->Auth->login([], 'POST', '', $user);
}
if ('' != $provider->userLogin) {
$names[] = $provider->userLogin;
// на форуме есть пользователь с таким email
if (
$this->c->providerUser->findByEmail($provider->userEmail) > 0
|| $this->c->users->loadByEmail($provider->userEmail) instanceof User
) {
$auth = $this->c->Auth;
$auth->fIswev = [FORK_MESS_INFO, ['Email message', __($provider->name)]];
return $auth->forget([], 'GET', $provider->userEmail);
}
if ('' != ($tmp = (string) \strstr($provider->userEmail, '@', true))) {
$names[] = $tmp;
// регистрация закрыта
if (1 !== $this->c->config->b_regs_allow) {
return $this->c->Message->message('No new regs');
}
$names[] = 'user' . \time();
$v = $this->c->Validator->reset()->addRules(['name' => 'required|string:trim|username|noURL:1']);
$end = '';
$i = 0;
// продолжение регистрации начиная с согласия с правилами
if ('reg' !== $provider->stateType) {
$page = $this->c->Rules->confirmation();
$form = $page->form;
while ($i < 100) {
foreach ($names as $name) {
if ($v->validation(['name' => $name . $end])) {
return $v->name;
}
}
$form['hidden']['oauth'] = $this->providerToString($provider);
$end = '_' . $this->c->Secury->randomHash(4);
++$i;
$page->form = $form;
$page->fIswev = [FORK_MESS_INFO, 'First time Register?'];
return $page;
}
throw new RuntimeException('Failed to generate unique username');
return $this->c->Register->reg([], 'POST', $provider);
}
}

View file

@ -11,10 +11,14 @@ declare(strict_types=1);
namespace ForkBB\Models\Pages;
use ForkBB\Models\Model;
use ForkBB\Models\Provider\Driver;
use function \ForkBB\__;
trait RegLogTrait
{
/**
* Подготавливает массив данных для формы
*/
protected function reglogForm(string $type): array
{
if (
@ -61,4 +65,81 @@ trait RegLogTrait
'btns' => $btns,
];
}
/**
* Кодирует данные провайдера(пользователя) в строку
*/
protected function providerToString(Driver $provider): string
{
$data = [
'name' => $provider->name,
'userInfo' => $provider->userInfo,
];
$data = \base64_encode(\json_encode($data, FORK_JSON_ENCODE));
$hash = $this->c->Secury->hash($data);
return "{$data}:{$hash}";
}
/**
* Раскодирует данные провайдера(пользователя) из строку или false
*/
protected function stringToProvider(string $data): Driver|false
{
$data = \explode(':', $data);
if (2 !== \count($data)) {
return false;
}
if (
! \hash_equals($data[1], $this->c->Secury->hash($data[0]))
|| ! \is_array($data = \json_decode(\base64_decode($data[0], true), true))
) {
return false;
}
$provider = $this->c->providers->init()->get($data['name']);
$provider->userInfo = $data['userInfo'];
return $provider;
}
/**
* Подбирает уникальное имя для регистрации пользователя
*/
protected function nameGenerator(Driver $provider): string
{
$names = [];
if ('' != $provider->userName) {
$names[] = $provider->userName;
}
if ('' != $provider->userLogin) {
$names[] = $provider->userLogin;
}
if ('' != ($tmp = (string) \strstr($provider->userEmail, '@', true))) {
$names[] = $tmp;
}
$v = (clone $this->c->Validator)->reset()->addRules(['name' => 'required|string:trim|username|noURL:1']);
$end = '';
$i = 0;
while ($i < 3) {
foreach ($names as $name) {
if ($v->validation(['name' => $name . $end])) {
return $v->name;
}
}
$end = '_' . $this->c->Secury->randomHash(4);
++$i;
}
return 'user' . \time();
}
}

View file

@ -10,10 +10,12 @@ declare(strict_types=1);
namespace ForkBB\Models\Pages;
use ForkBB\Core\Image;
use ForkBB\Core\Validator;
use ForkBB\Core\Exceptions\MailException;
use ForkBB\Models\Page;
use ForkBB\Models\Pages\RegLogTrait;
use ForkBB\Models\Provider\Driver;
use ForkBB\Models\User\User;
use function \ForkBB\__;
@ -21,25 +23,65 @@ class Register extends Page
{
use RegLogTrait;
/**
* Флаг входа с помощью OAuth
*/
protected bool $useOAuth = false;
/**
* Регистрация
*/
public function reg(): Page
public function reg(array $args, string $method, Driver $provider = null): Page
{
$this->c->Lang->load('validator');
$this->c->Lang->load('register');
// регистрация через OAuth
if (null !== $provider) {
$this->provider = $provider;
$_POST = [
'token' => $this->c->Csrf->create('RegisterForm'),
'agree' => $this->c->Csrf->create('Register'),
'oauth' => $this->providerToString($provider),
'register' => 'Register with OAuth',
];
// переход от Rules/завершение регистрации через OAuth
} else {
$v = $this->c->Validator->reset()->addRules(['oauth' => 'string']);
if (
! $v->validation($_POST)
|| (
null !== $v->oauth
&& ! ($this->provider = $this->stringToProvider($v->oauth)) instanceof Driver
)
) {
return $this->c->Message->message('Bad request');
}
}
$this->useOAuth = $this->provider instanceof Driver;
$rules = [
'token' => 'token:RegisterForm',
'agree' => 'required|token:Register',
'on' => 'integer',
'oauth' => 'string',
'email' => 'required_with:on|string:trim|email:noban',
'username' => 'required_with:on|string:trim|username|noURL:1',
'password' => 'required_with:on|string|min:16|max:100000|password',
'register' => 'required|string',
];
if ($this->useOAuth) {
unset($rules['email'], $rules['password']);
}
$v = $this->c->Validator->reset()
->addValidators([
])->addRules([
'token' => 'token:RegisterForm',
'agree' => 'required|token:Register',
'on' => 'integer',
'email' => 'required_with:on|string:trim|email:noban',
'username' => 'required_with:on|string:trim|username|noURL:1',
'password' => 'required_with:on|string|min:16|max:100000|password',
'register' => 'required|string',
])->addAliases([
->addValidators([])
->addRules($rules)
->addAliases([
'email' => 'Email',
'username' => 'Username',
'password' => 'Passphrase',
@ -55,23 +97,24 @@ class Register extends Page
if ($v->validation($_POST, true)) {
// завершение регистрации
if (1 === $v->on) {
$userInDB = $this->c->users->loadByEmail($v->email);
$email = $this->useOAuth ? $this->provider->userEmail : $v->email;
$userInDB = $this->c->users->loadByEmail($email);
if ($userInDB instanceof User) {
return $this->regDupe($v, $userInDB);
return $this->regDupe($v, $userInDB, $email);
}
$id = $this->c->providerUser->findByEmail($v->email);
$id = $this->c->providerUser->findByEmail($email);
if ($id > 0) {
$userInDB = $this->c->users->load($id);
if ($userInDB instanceof User) {
return $this->regDupe($v, $userInDB);
return $this->regDupe($v, $userInDB, $email);
}
}
return $this->regEnd($v);
return $this->regEnd($v, $email);
}
} else {
$this->fIswev = $v->getErrors();
@ -98,7 +141,7 @@ class Register extends Page
$this->titles = 'Register';
$this->robots = 'noindex';
$this->form = $this->formReg($v);
$this->formOAuth = $this->reglogForm('reg');
$this->formOAuth = $this->useOAuth ? null : $this->reglogForm('reg');
return $this;
}
@ -108,49 +151,14 @@ class Register extends Page
*/
protected function formReg(Validator $v): array
{
return [
$form = [
'action' => $this->c->Router->link('RegisterForm'),
'hidden' => [
'token' => $this->c->Csrf->create('RegisterForm'),
'agree' => $v->agree,
'on' => '1',
],
'sets' => [
'reg' => [
'fields' => [
'email' => [
'autofocus' => true,
'class' => ['hint'],
'type' => 'text',
'maxlength' => (string) $this->c->MAX_EMAIL_LENGTH,
'value' => $v->email,
'caption' => 'Email',
'help' => 1 === $this->c->config->b_regs_verify ? 'Email help2' : 'Email help',
'required' => true,
'pattern' => '.+@.+',
'autocapitalize' => 'off',
],
'username' => [
'class' => ['hint'],
'type' => 'text',
'maxlength' => '25',
'value' => $v->username,
'caption' => 'Username',
'help' => 'Login format',
'required' => true,
'pattern' => '^.{2,25}$',
],
'password' => [
'class' => ['hint'],
'type' => 'password',
'caption' => 'Passphrase',
'help' => 'Passphrase help',
'required' => true,
'pattern' => '^.{16,}$',
],
],
],
],
'sets' => [],
'btns' => [
'register' => [
'type' => 'submit',
@ -158,14 +166,64 @@ class Register extends Page
],
],
];
$fields = [];
if (! $this->useOAuth) {
$fields['email'] = [
'autofocus' => true,
'class' => ['hint'],
'type' => 'text',
'maxlength' => (string) $this->c->MAX_EMAIL_LENGTH,
'value' => $v->email,
'caption' => 'Email',
'help' => 1 === $this->c->config->b_regs_verify ? 'Email help2' : 'Email help',
'required' => true,
'pattern' => '.+@.+',
'autocapitalize' => 'off',
];
}
$fields['username'] = [
'class' => ['hint'],
'type' => 'text',
'maxlength' => '25',
'value' => $v->username ?? ($this->useOAuth ? $this->nameGenerator($this->provider) : ''),
'caption' => 'Username',
'help' => 'Login format',
'required' => true,
'pattern' => '^.{2,25}$',
];
if (! $this->useOAuth) {
$fields['password'] = [
'class' => ['hint'],
'type' => 'password',
'caption' => 'Passphrase',
'help' => 'Passphrase help',
'required' => true,
'pattern' => '^.{16,}$',
];
}
$form['sets']['reg']['fields'] = $fields;
if ($this->useOAuth) {
$form['hidden']['oauth'] = $v->oauth;
}
return $form;
}
/**
* Завершение регистрации
*/
protected function regEnd(Validator $v): Page
protected function regEnd(Validator $v, string $email): Page
{
if (1 === $this->c->config->b_regs_verify) {
if (
! $this->useOAuth
&& 1 === $this->c->config->b_regs_verify
) {
$groupId = FORK_GROUP_UNVERIFIED;
$key = $this->c->Secury->randomPass(31);
} else {
@ -176,10 +234,10 @@ class Register extends Page
$user = $this->c->users->create();
$user->username = $v->username;
$user->password = \password_hash($v->password, \PASSWORD_DEFAULT);
$user->password = $this->useOAuth ? 'oauth_' . $this->c->Secury->randomPass(7) : \password_hash($v->password, \PASSWORD_DEFAULT);
$user->group_id = $groupId;
$user->email = $v->email;
$user->email_confirmed = 0;
$user->email = $email;
$user->email_confirmed = $this->useOAuth && $this->provider->userEmailVerifed ? 1 : 0;
$user->activate_string = $key;
$user->u_mark_all_read = \time();
$user->email_setting = $this->c->config->i_default_email_setting;
@ -189,9 +247,50 @@ class Register extends Page
$user->registered = \time();
$user->registration_ip = $this->user->ip;
$user->signature = '';
$user->ip_check_type = 0;
$user->location = $this->useOAuth ? $this->provider->userLocation : '';
$user->url = $this->useOAuth ? $this->provider->userURL : '';
if (
$this->useOAuth
&& $this->provider->userAvatar
) {
$image = $this->c->Files->uploadFromLink($this->provider->userAvatar);
if ($image instanceof Image) {
$name = $this->c->Secury->randomPass(8);
$path = $this->c->DIR_PUBLIC . "{$this->c->config->o_avatars_dir}/{$name}.(webp|jpg|png|gif)";
$result = $image
->rename(true)
->rewrite(false)
->resize($this->c->config->i_avatars_width, $this->c->config->i_avatars_height)
->toFile($path, $this->c->config->i_avatars_size);
if (true === $result) {
$user->avatar = $image->name() . '.' . $image->ext();
} else {
$this->c->Log->warning('OAuth Failed image processing', [
'user' => $user->fLog(),
'error' => $image->error(),
]);
}
} else {
$this->c->Log->warning('OAuth Avatar not image', [
'user' => $user->fLog(),
'error' => $this->c->Files->error(),
]);
}
}
$newUserId = $this->c->users->insert($user);
if (
$this->useOAuth
&& true !== $this->c->providerUser->registration($user, $this->provider)
) {
throw new RuntimeException('Failed to insert data'); // ??????????????????????????????????????????
}
$this->c->Log->info('Registriaton: ok', [
'user' => $user->fLog(),
'form' => $v->getData(false, ['token', 'agree', 'password']),
@ -240,7 +339,10 @@ class Register extends Page
$this->c->Lang->load('register');
// отправка письма активации аккаунта
if (1 === $this->c->config->b_regs_verify) {
if (
! $this->useOAuth
&& 1 === $this->c->config->b_regs_verify
) {
$this->c->Csrf->setHashExpiration(259200); // ???? хэш действует 72 часа
$link = $this->c->Router->link(
@ -264,7 +366,7 @@ class Register extends Page
->setMaxRecipients(1)
->setFolder($this->c->DIR_LANG)
->setLanguage($this->user->language)
->setTo($v->email)
->setTo($email)
->setFrom($this->c->config->o_webmaster_email, $tplData['fMailer'])
->setTpl('welcome.tpl', $tplData)
->send();
@ -286,21 +388,24 @@ class Register extends Page
$auth = $this->c->Auth;
$auth->fIswev = [FORK_MESS_WARN, ['Error welcom mail', $this->c->config->o_admin_email]];
return $auth->forget([], 'GET', $v->email);
return $auth->forget([], 'GET', $email);
}
// форма логина
} else {
return $this->c->Auth->login([], 'POST', '', $user);
/*
$auth = $this->c->Auth;
$auth->fIswev = [FORK_MESS_SUCC, 'Reg complete'];
return $auth->login([], 'GET', $v->username);
*/
}
}
/**
* Делает вид, что пользователь зарегистрирован (для предотвращения утечки email)
*/
protected function regDupe(Validator $v, User $userInDB): Page
protected function regDupe(Validator $v, User $userInDB, string $email): Page
{
$this->c->Log->warning('Registriaton: dupe', [
'user' => $this->user->fLog(), // ????
@ -346,7 +451,10 @@ class Register extends Page
$this->c->Lang->load('register');
// фейк отправки письма активации аккаунта
if (1 === $this->c->config->b_regs_verify) {
if (
! $this->useOAuth
&& 1 === $this->c->config->b_regs_verify
) {
$isSent = true;
// письмо активации аккаунта отправлено
@ -357,7 +465,7 @@ class Register extends Page
$auth = $this->c->Auth;
$auth->fIswev = [FORK_MESS_WARN, ['Error welcom mail', $this->c->config->o_admin_email]];
return $auth->forget([], 'GET', $v->email);
return $auth->forget([], 'GET', $email);
}
// форма логина
} else {

View file

@ -119,3 +119,6 @@ msgstr "Email verified"
msgid "Account removed"
msgstr "Account removed."
msgid "First time Register?"
msgstr "You are here for the first time. Register?"

View file

@ -119,3 +119,6 @@ msgstr "Адрес проверен"
msgid "Account removed"
msgstr "Аккаунт удален."
msgid "First time Register?"
msgstr "Вы здесь впервые. Зарегистрируетесь?"