Visman преди 2 години
родител
ревизия
8b72a4a859

+ 30 - 0
app/Controllers/Routing.php

@@ -94,6 +94,24 @@ class Routing
                 'Redirect:toIndex'
                 'Redirect:toIndex'
             );
             );
         }
         }
+        // OAuth
+        if (
+            $user->isAdmin
+            || 1 === $config->b_oauth_allow
+        ) {
+            $r->add(
+                $r::GET,
+                '/reglog/callback/{name}',
+                'RegLog:callback',
+                'RegLogCallback'
+            );
+            $r->add(
+                $r::PST,
+                '/reglog/redirect',
+                'RegLog:redirect',
+                'RegLogRedirect'
+            );
+        }
         // просмотр разрешен
         // просмотр разрешен
         if (1 === $user->g_read_board) {
         if (1 === $user->g_read_board) {
             // главная
             // главная
@@ -557,6 +575,18 @@ class Routing
                 'AdminOptions:edit',
                 'AdminOptions:edit',
                 'AdminOptions'
                 'AdminOptions'
             );
             );
+            $r->add(
+                $r::DUO,
+                '/admin/options/providers',
+                'AdminProviders:view',
+                'AdminProviders'
+            );
+            $r->add(
+                $r::DUO,
+                '/admin/options/providers/{name}',
+                'AdminProviders:edit',
+                'AdminProvider'
+            );
             $r->add(
             $r->add(
                 $r::DUO,
                 $r::DUO,
                 '/admin/parser',
                 '/admin/parser',

+ 14 - 0
app/Models/Pages/Admin/Options.php

@@ -110,6 +110,7 @@ class Options extends Admin
                     'i_poll_term'             => 'required|integer|min:0|max:99',
                     'i_poll_term'             => 'required|integer|min:0|max:99',
                     'b_poll_guest'            => 'required|integer|in:0,1',
                     'b_poll_guest'            => 'required|integer|in:0,1',
                     'b_pm'                    => 'required|integer|in:0,1',
                     'b_pm'                    => 'required|integer|in:0,1',
+                    'b_oauth_allow'           => 'required|integer|in:0,1',
                 ])->addAliases([
                 ])->addAliases([
                 ])->addArguments([
                 ])->addArguments([
                 ])->addMessages([
                 ])->addMessages([
@@ -610,6 +611,19 @@ class Options extends Admin
                     'caption' => 'Report new label',
                     'caption' => 'Report new label',
                     'help'    => 'Report new help',
                     'help'    => 'Report new help',
                 ],
                 ],
+                'b_oauth_allow' => [
+                    'type'    => 'radio',
+                    'value'   => $config->b_oauth_allow,
+                    'values'  => $yn,
+                    'caption' => 'Allow oauth label',
+                    'help'    => 'Allow oauth help',
+                ],
+                'configure_providers' => [
+                    'type'  => 'link',
+                    'value' => __('Configure providers'),
+                    'href'  => $this->c->Router->link('AdminProviders'),
+                    'title' => __('Configure providers'),
+                ],
                 'b_rules' => [
                 'b_rules' => [
                     'type'    => 'radio',
                     'type'    => 'radio',
                     'value'   => $config->b_rules,
                     'value'   => $config->b_rules,

+ 234 - 0
app/Models/Pages/Admin/Providers.php

@@ -0,0 +1,234 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Models\Pages\Admin;
+
+use ForkBB\Core\Container;
+use ForkBB\Models\Page;
+use ForkBB\Models\Pages\Admin;
+use ForkBB\Models\Provider\Driver;
+use function \ForkBB\__;
+
+class Providers extends Admin
+{
+    /**
+     * Выводит сообщение
+     */
+    protected function mDisabled(): void
+    {
+        if (1 !== $this->c->config->b_oauth_allow) {
+            $this->fIswev = ['w', ['OAuth authorization disabled', $this->c->Router->link('AdminOptions', ['#' => 'id-fs-registration'])]];
+        }
+    }
+
+    /**
+     * Просмотр, редактирвоание и добавление категорий
+     */
+    public function view(array $args, string $method): Page
+    {
+        $this->c->Lang->load('validator');
+        $this->c->Lang->load('admin_providers');
+
+        if ('POST' === $method) {
+            $v = $this->c->Validator->reset()
+                ->addRules([
+                    'token'           => 'token:AdminProviders',
+                    'form.*.pr_pos'   => 'required|integer|min:0|max:9999999999',
+                    'form.*.pr_allow' => 'checkbox',
+                ])->addAliases([
+                ])->addArguments([
+                ])->addMessages([
+                ]);
+
+            if ($v->validation($_POST)) {
+                $this->c->providers->update($v->form);
+
+                return $this->c->Redirect->page('AdminProviders')->message('Providers updated redirect');
+            }
+
+            $this->fIswev  = $v->getErrors();
+        }
+
+        $this->mDisabled();
+
+        $this->nameTpl   = 'admin/form';
+        $this->aIndex    = 'options';
+        $this->aCrumbs[] = [$this->c->providers->link(''), 'Providers'];
+        $this->form      = $this->formView();
+        $this->classForm = ['providers', 'inline'];
+        $this->titleForm = 'Providers';
+
+        return $this;
+    }
+
+    /**
+     * Подготавливает массив данных для формы
+     */
+    protected function formView(): array
+    {
+        $form = [
+            'action' => $this->c->providers->link(''),
+            'hidden' => [
+                'token' => $this->c->Csrf->create('AdminProviders'),
+            ],
+            'sets'   => [],
+            'btns'   => [
+                'save' => [
+                    'type'  => 'submit',
+                    'value' => __('Save changes'),
+                ],
+            ],
+        ];
+        $fields = [];
+
+        foreach ($this->c->providers->init()->list() as $provider) {
+            $fields["name-{$provider->name}"] = [
+                'class'   => ['name', 'provider'],
+                'type'    => 'btn',
+                'value'   => $provider->name,
+                'caption' => 'Provider label',
+                'link'    => $this->c->providers->link($provider->name),
+            ];
+            $fields["form[{$provider->name}][pr_pos]"] = [
+                'class'   => ['position', 'provider'],
+                'type'    => 'number',
+                'min'     => '0',
+                'max'     => '9999999999',
+                'value'   => $provider->pos,
+                'caption' => 'Position label',
+            ];
+            $fields["form[{$provider->name}][pr_allow]"] = [
+                'class'   => ['allow', 'provider'],
+                'type'    => 'checkbox',
+                'checked' => $provider->allow,
+                'caption' => 'Allow label',
+            ];
+            $form['sets']["provider-{$provider->name}"] = [
+                'class'  => ['provider', 'inline'],
+                'legend' => $provider->name,
+                'fields' => $fields,
+            ];
+        }
+
+        return $form;
+    }
+
+    /**
+     * Просмотр, редактирвоание и добавление категорий
+     */
+    public function edit(array $args, string $method): Page
+    {
+        $provider = $this->c->providers->init()->get($args['name']);
+
+        if (! $provider instanceof Driver) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        $this->c->Lang->load('validator');
+        $this->c->Lang->load('admin_providers');
+
+        if ('POST' === $method) {
+            $v = $this->c->Validator->reset()
+                ->addRules([
+                    'token'         => 'token:AdminProvider',
+                    'client_id'     => 'exist|string:trim|max:255',
+                    'client_secret' => 'exist|string:trim|max:255',
+                    'changeData'    => 'checkbox',
+                ])->addAliases([
+                ])->addArguments([
+                    'token' => $args,
+                ])->addMessages([
+                ]);
+
+            if ($v->validation($_POST)) {
+                if ($v->changeData) {
+                    $this->c->providers->update([
+                        $provider->name => [
+                            'pr_cl_id'  => $v->client_id,
+                            'pr_cl_sec' => $v->client_secret,
+                         ],
+                    ]);
+
+                    $message = 'Provider updated redirect';
+                } else {
+                    $message = 'No confirm redirect';
+                }
+
+                return $this->c->Redirect->page('AdminProvider', $args)->message($message);
+            }
+
+            $this->fIswev  = $v->getErrors();
+        }
+
+        $this->mDisabled();
+
+        $this->nameTpl   = 'admin/form';
+        $this->aIndex    = 'options';
+        $this->aCrumbs[] = [$this->c->providers->link($provider->name), $provider->name];
+        $this->aCrumbs[] = [$this->c->providers->link(''), 'Providers'];
+        $this->form      = $this->formEdit($provider);
+        $this->classForm = ['provider'];
+        $this->titleForm = $provider->name;
+
+        return $this;
+    }
+
+    /**
+     * Подготавливает массив данных для формы
+     */
+    protected function formEdit(Driver $provider): array
+    {
+        $form = [
+            'action' => $this->c->providers->link($provider->name),
+            'hidden' => [
+                'token' => $this->c->Csrf->create('AdminProvider', ['name' => $provider->name]),
+            ],
+            'sets'   => [
+                'provider' => [
+                    'fields' => [
+                        'callback'      => [
+                            'type'      => 'str',
+                            'value'     => $provider->linkCallback,
+                            'caption'   => 'Callback label',
+                            'help'      => 'Callback help',
+                        ],
+                        'client_id'     => [
+                            'type'      => 'text',
+                            'maxlength' => '255',
+                            'value'     => $provider->client_id,
+                            'caption'   => 'ID label',
+                            'help'      => 'ID help',
+                        ],
+                        'client_secret' => [
+                            'type'      => 'text',
+                            'maxlength' => '255',
+                            'value'     => '' == $provider->client_secret ? '' : '********',
+                            'caption'   => 'Secret label',
+                            'help'      => 'Secret help',
+                        ],
+                        'changeData'    => [
+                            'type'      => 'checkbox',
+                            'caption'   => '',
+                            'label'     => 'Change data help',
+                        ],
+                    ],
+                ]
+            ],
+            'btns'   => [
+                'save' => [
+                    'type'  => 'submit',
+                    'value' => __('Save changes'),
+                ],
+            ],
+        ];
+
+        return $form;
+    }
+}

+ 95 - 1
app/Models/Pages/Admin/Update.php

@@ -25,7 +25,7 @@ class Update extends Admin
 {
 {
     const PHP_MIN                    = '8.0.0';
     const PHP_MIN                    = '8.0.0';
     const REV_MIN_FOR_UPDATE         = 53;
     const REV_MIN_FOR_UPDATE         = 53;
-    const LATEST_REV_WITH_DB_CHANGES = 53;
+    const LATEST_REV_WITH_DB_CHANGES = 55;
     const LOCK_NAME                  = 'lock_update';
     const LOCK_NAME                  = 'lock_update';
     const LOCK_TTL                   = 1800;
     const LOCK_TTL                   = 1800;
     const JSON_OPTIONS               = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR;
     const JSON_OPTIONS               = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR;
@@ -410,4 +410,98 @@ class Update extends Admin
 #
 #
 #        return null;
 #        return null;
 #    }
 #    }
+
+    /**
+     * rev.54 to rev.55
+     */
+    protected function stageNumber54(array $args): ?int
+    {
+        $config = $this->c->config;
+
+        $config->b_oauth_allow = 0;
+
+        $config->save();
+
+        $schema = [
+            'FIELDS' => [
+                'pr_name'     => ['VARCHAR(25)', false],
+                'pr_allow'    => ['TINYINT(1)', false, 0],
+                'pr_pos'      => ['INT(10) UNSIGNED', false, 0],
+                'pr_cl_id'    => ['VARCHAR(255)', false, ''],
+                'pr_cl_sec'   => ['VARCHAR(255)', false, ''],
+            ],
+            'UNIQUE KEYS' => [
+                'pr_name_idx' => ['pr_name'],
+            ],
+        ];
+        $this->c->DB->createTable('::providers', $schema);
+
+        $schema = [
+            'FIELDS' => [
+                'uid'               => ['INT(10) UNSIGNED', false],
+                'pr_name'           => ['VARCHAR(25)', false],
+                'pu_uid'            => ['VARCHAR(165)', false],
+                'pu_email'          => ['VARCHAR(190)', false, ''],
+                'pu_email_normal'   => ['VARCHAR(190)', false, ''],
+                'pu_email_verified' => ['TINYINT(1)', false, 0],
+            ],
+            'UNIQUE KEYS' => [
+                'pr_name_pu_uid_idx' => ['pr_name', 'pu_uid'],
+            ],
+            'INDEXES' => [
+                'uid_idx'             => ['uid'],
+                'pu_email_normal_idx' => ['pu_email_normal'],
+            ],
+        ];
+        $this->c->DB->createTable('::providers_users', $schema);
+
+        $providers = [
+            'github',
+        ];
+
+        $query = 'INSERT INTO ::providers (pr_name, pr_pos)
+            SELECT tmp.*
+            FROM (SELECT ?s:name AS f1, ?i:pos AS f2) AS tmp
+            WHERE NOT EXISTS (
+                SELECT 1
+                FROM ::providers
+                WHERE pr_name=?s:name
+            )';
+
+        foreach ($providers as $pos => $name) {
+            $vars = [
+                ':name' => $name,
+                ':pos'  => $pos,
+            ];
+
+            $this->c->DB->exec($query, $vars);
+        }
+
+        $coreConfig = new CoreConfig($this->configFile);
+
+        $coreConfig->add(
+            'multiple=>AdminProviders',
+            '\\ForkBB\\Models\\Pages\\Admin\\Providers::class',
+            'AdminOptions'
+        );
+        $coreConfig->add(
+            'shared=>providers',
+            [
+                'class'   => '\\ForkBB\\Models\\Provider\\Providers::class',
+                'drivers' => [
+                    'github' => '\\ForkBB\\Models\\Provider\\Driver\\GitHub::class'
+                ],
+            ],
+            'pms'
+        );
+        $coreConfig->add(
+            'multiple=>RegLog',
+            '\\ForkBB\\Models\\Pages\\RegLog::class',
+            'Register'
+        );
+
+        $coreConfig->save();
+
+        return null;
+    }
 }
 }

+ 4 - 0
app/Models/Pages/Auth.php

@@ -13,12 +13,15 @@ namespace ForkBB\Models\Pages;
 use ForkBB\Core\Validator;
 use ForkBB\Core\Validator;
 use ForkBB\Core\Exceptions\MailException;
 use ForkBB\Core\Exceptions\MailException;
 use ForkBB\Models\Page;
 use ForkBB\Models\Page;
+use ForkBB\Models\Pages\RegLogTrait;
 use ForkBB\Models\User\User;
 use ForkBB\Models\User\User;
 use SensitiveParameter;
 use SensitiveParameter;
 use function \ForkBB\__;
 use function \ForkBB\__;
 
 
 class Auth extends Page
 class Auth extends Page
 {
 {
+    use RegLogTrait;
+
     /**
     /**
      * Выход пользователя
      * Выход пользователя
      */
      */
@@ -105,6 +108,7 @@ class Auth extends Page
         $save               = $v->save ?? true;
         $save               = $v->save ?? true;
         $redirect           = $v->redirect ?? $this->c->Router->validate($ref, 'Index');
         $redirect           = $v->redirect ?? $this->c->Router->validate($ref, 'Index');
         $this->form         = $this->formLogin($username, (bool) $save, $redirect);
         $this->form         = $this->formLogin($username, (bool) $save, $redirect);
+        $this->formOAuth    = $this->reglogForm();
 
 
         return $this;
         return $this;
     }
     }

+ 82 - 0
app/Models/Pages/RegLog.php

@@ -0,0 +1,82 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Models\Pages;
+
+use ForkBB\Core\Validator;
+use ForkBB\Core\Exceptions\MailException;
+use ForkBB\Models\Page;
+use ForkBB\Models\User\User;
+use function \ForkBB\__;
+
+class RegLog extends Page
+{
+    /**
+     * Обрабатывает нажатие одной из кнопок провайдеров
+     */
+    public function redirect(): Page
+    {
+        if (
+            1 !== $this->c->config->b_oauth_allow
+            || empty($list = $this->c->providers->active())
+        ) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        $rules = [
+            'token' => 'token:RegLogRedirect',
+        ];
+
+        foreach ($list as $name) {
+            $rules[$name] = 'string';
+        }
+
+        $v = $this->c->Validator->reset()->addRules($rules);
+
+        if (
+            ! $v->validation($_POST)
+            || 1 !== \count($form = $v->getData(false, ['token']))
+        ) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        return $this->c->Redirect->url($this->c->providers->init()->get(\array_key_first($form))->linkAuth);
+    }
+
+    /**
+     * Обрабатывает ответ сервера
+     */
+    public function callback(array $args): Page
+    {
+        if (
+            1 !== $this->c->config->b_oauth_allow
+            || empty($list = $this->c->providers->active())
+            || empty($list[$args['name']])
+        ) {
+            return $this->c->Message->message('Bad request');
+        }
+
+        $this->c->Lang->load('admin_providers');
+
+        $provider = $this->c->providers->init()->get($args['name']);
+
+        if (true !== ($result = $provider->verifyAuth($_GET))) {
+            return $this->c->Message->message($result);
+        }
+
+        if (true !== $provider->reqAccessToken()) {
+            return $this->c->Message->message('Error token');
+        }
+
+        if (true !== $provider->reqUserInfo()) {
+
+        }
+    }
+}

+ 47 - 0
app/Models/Pages/RegLogTrait.php

@@ -0,0 +1,47 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Models\Pages;
+
+use ForkBB\Models\Model;
+use function \ForkBB\__;
+
+trait RegLogTrait
+{
+    protected function reglogForm(): array
+    {
+        if (
+            1 !== $this->c->config->b_oauth_allow
+            || empty($list = $this->c->providers->active())
+        ) {
+            return [];
+        }
+
+        $this->c->Lang->load('admin_providers');
+
+        $btns = [];
+
+        foreach ($list as $name) {
+            $btns[$name] = [
+                'type'  => 'submit',
+                'value' => __(['Sign in with %s', __($name)]),
+            ];
+        }
+
+        return [
+            'action' => $this->c->Router->link('RegLogRedirect'),
+            'hidden' => [
+                'token' => $this->c->Csrf->create('RegLogRedirect'),
+            ],
+            'sets'   => [],
+            'btns'   => $btns,
+        ];
+    }
+}

+ 4 - 0
app/Models/Pages/Register.php

@@ -13,11 +13,14 @@ namespace ForkBB\Models\Pages;
 use ForkBB\Core\Validator;
 use ForkBB\Core\Validator;
 use ForkBB\Core\Exceptions\MailException;
 use ForkBB\Core\Exceptions\MailException;
 use ForkBB\Models\Page;
 use ForkBB\Models\Page;
+use ForkBB\Models\Pages\RegLogTrait;
 use ForkBB\Models\User\User;
 use ForkBB\Models\User\User;
 use function \ForkBB\__;
 use function \ForkBB\__;
 
 
 class Register extends Page
 class Register extends Page
 {
 {
+    use RegLogTrait;
+
     /**
     /**
      * Регистрация
      * Регистрация
      */
      */
@@ -85,6 +88,7 @@ class Register extends Page
         $this->titles       = 'Register';
         $this->titles       = 'Register';
         $this->robots       = 'noindex';
         $this->robots       = 'noindex';
         $this->form         = $this->formReg($v);
         $this->form         = $this->formReg($v);
+        $this->formOAuth    = $this->reglogForm();
 
 
         return $this;
         return $this;
     }
     }

+ 162 - 0
app/Models/Provider/Driver.php

@@ -0,0 +1,162 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Models\Provider;
+
+use ForkBB\Core\Container;
+use ForkBB\Models\Model;
+use RuntimeException;
+
+abstract class Driver extends Model
+{
+    /**
+     * Ключ модели для контейнера
+     */
+    protected string $cKey = 'Provider';
+
+    protected string $code;
+
+    protected string $originalName;
+    protected string $authURL;
+    protected string $tokenURL;
+    protected string $userURL;
+    protected string $scope;
+
+    public function __construct(protected string $client_id, protected string $client_secret, Container $c)
+    {
+        parent::__construct($c);
+    }
+
+    protected function setname(string $name):void
+    {
+        if ($this->originalName !== $name) {
+            throw new RuntimeException("Invalid name: {$name}");
+        }
+
+        $this->setAttr('name', $name);
+    }
+
+    protected function getlinkCallback(): string
+    {
+        return $this->c->Router->link('RegLogCallback', ['name' => $this->name]);
+    }
+
+    protected function getclient_id(): string
+    {
+        return $this->client_id;
+    }
+
+    protected function getclient_secret(): string
+    {
+        return $this->client_secret;
+    }
+
+    protected function getlinkAuth(): string
+    {
+        $params = [
+            'response_type' => 'code',
+            'scope'         => $this->scope,
+            'state'         => $this->c->Csrf->createHash($this->originalName, ['ip' => $this->c->user->ip]),
+            'client_id'     => $this->client_id,
+            'redirect_uri'  => $this->linkCallback,
+        ];
+
+        return $this->authURL . '?' . \http_build_query($params);
+    }
+
+    protected function verifyState(string $state): bool
+    {
+        return $this->c->Csrf->verify($state, $this->originalName, ['ip' => $this->c->user->ip]);
+    }
+
+    public function verifyAuth(array $data): bool|string|array
+    {
+        if (! \is_string($data['code'] ?? null)) {
+            if (\is_string($data['error'] ?? null)) {
+                return ['Provider response: %s', $data['error']];
+            } else {
+                return ['Provider response: %s', 'undefined'];
+            }
+        }
+
+        if (
+            ! \is_string($data['state'] ?? null)
+            || ! $this->verifyState($data['state'])
+        ) {
+            return 'State error';
+        }
+
+        $this->code = $data['code'];
+
+        return true;
+    }
+
+    public function reqAccessToken(): bool
+    {
+        $params = [
+            'grant_type'    => 'authorization_code',
+            'client_id'     => $this->client_id,
+            'client_secret' => $this->client_secret,
+            'code'          => $this->code,
+            'redirect_uri'  => $this->linkCallback,
+        ];
+
+        $ch = \curl_init($this->tokenURL);
+
+        \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Accept: application/json']);
+        \curl_setopt($ch, \CURLOPT_POST, true);
+        \curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query($params));
+        \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
+        \curl_setopt($ch, \CURLOPT_HEADER, false);
+
+        $html = \curl_exec($ch);
+#        $type = \curl_getinfo($ch, \CURLINFO_CONTENT_TYPE);
+
+        \curl_close($ch);
+
+        if (
+            ! isset($html[1])
+            || '{' !== $html[0]
+            || '}' !== $html[-1]
+            || ! \is_array($json = \json_decode($html, true, 20, \JSON_BIGINT_AS_STRING & JSON_UNESCAPED_UNICODE & JSON_INVALID_UTF8_SUBSTITUTE))
+            || ! isset($json['access_token'])
+        ) {
+            return false;
+        }
+
+        $this->access_token = $json['access_token'];
+
+        return true;
+    }
+
+    public function reqUserInfo(): bool
+    {
+        $headers = [
+            'Accept: application/json',
+            "Authorization: Bearer {$this->access_token}",
+            'User-Agent: ForkBB (Client ID: {$this->client_id})',
+        ];
+
+        $ch = \curl_init($this->userURL);
+
+        \curl_setopt($ch, \CURLOPT_HTTPHEADER, $headers);
+        \curl_setopt($ch, \CURLOPT_POST, false);
+        #\curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query($params));
+        \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
+        \curl_setopt($ch, \CURLOPT_HEADER, false);
+
+        $html = \curl_exec($ch);
+#        $type = \curl_getinfo($ch, \CURLINFO_CONTENT_TYPE);
+
+        \curl_close($ch);
+
+        exit(var_dump("<pre>".$html));
+    }
+}

+ 24 - 0
app/Models/Provider/Driver/GitHub.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Models\Provider\Driver;
+
+use ForkBB\Models\Provider\Driver;
+use RuntimeException;
+
+class GitHub extends Driver
+{
+    protected string $originalName = 'github';
+    protected string $authURL      = 'https://github.com/login/oauth/authorize';
+    protected string $tokenURL     = 'https://github.com/login/oauth/access_token';
+    protected string $userURL      = 'https://api.github.com/user';
+    protected string $scope        = 'read:user';
+
+}

+ 192 - 0
app/Models/Provider/Providers.php

@@ -0,0 +1,192 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Models\Provider;
+
+use ForkBB\Core\Container;
+use ForkBB\Models\Manager;
+use ForkBB\Models\Provider\Driver;
+use RuntimeException;
+
+class Providers extends Manager
+{
+    const CACHE_KEY = 'providers';
+
+    /**
+     * Ключ модели для контейнера
+     */
+    protected string $cKey = 'Providers';
+
+    protected ?array $cache = null;
+    protected bool $ready = false;
+
+    public function __construct(protected array $drivers, Container $c)
+    {
+        parent::__construct($c);
+    }
+
+    public function init(): self
+    {
+        if (! $this->ready) {
+            foreach ($this->cache() as $cur) {
+                $this->create($cur);
+            }
+
+            $this->ready = true;
+        }
+
+        return $this;
+    }
+
+    public function link(string $name): string
+    {
+        return '' === $name
+            ? $this->c->Router->link('AdminProviders')
+            : $this->c->Router->link('AdminProvider', ['name' => $name]);
+    }
+
+    public function create(array $attrs = []): Driver
+    {
+        if (! isset($attrs['pr_name'])) {
+            throw new RuntimeException('Provider name missing');
+        }
+
+        if (! isset($this->drivers[$attrs['pr_name']])) {
+            throw new RuntimeException("No driver for '{$attrs['pr_name']}' provider");
+        }
+
+        if ($this->isset($attrs['pr_name'])) {
+            throw new RuntimeException("Driver '{$attrs['pr_name']}' already exists");
+        }
+
+        $class         = $this->drivers[$attrs['pr_name']];
+        $driver        =  new $class($attrs['pr_cl_id'], $attrs['pr_cl_sec'], $this->c);
+        $driver->name  = $attrs['pr_name'];
+        $driver->pos   = $attrs['pr_pos'];
+        $driver->allow = $attrs['pr_allow'];
+
+        $this->set($driver->name, $driver);
+
+        return $driver;
+    }
+
+    public function list(): array
+    {
+        return $this->repository;
+    }
+
+    public function active(): array
+    {
+        $result = [];
+
+        foreach ($this->cache() as $cur) {
+            if (
+                $cur['pr_allow']
+                && '' != $cur['pr_cl_id']
+                && '' != $cur['pr_cl_sec']
+            ) {
+                $result[$cur['pr_name']] = $cur['pr_name'];
+            }
+        }
+
+        return $result;
+    }
+
+    protected function cache(): array
+    {
+        if (! \is_array($this->cache)) {
+            $this->cache = $this->c->Cache->get(self::CACHE_KEY);
+        }
+
+        if (! \is_array($this->cache)) {
+            $query = 'SELECT pr_name, pr_allow, pr_pos, pr_cl_id, pr_cl_sec
+                FROM ::providers
+                ORDER BY pr_pos';
+
+            $stmt = $this->c->DB->query($query);
+
+            while ($cur = $stmt->fetch()) {
+                $this->cache[$cur['pr_name']] = $cur;
+            }
+
+            if (true !== $this->c->Cache->set(self::CACHE_KEY, $this->cache)) {
+                throw new RuntimeException('Unable to write value to cache - ' . self::CACHE_KEY);
+            }
+        }
+
+        return $this->cache;
+    }
+
+    protected function emptyField(string $key, array $sets, array $cache): bool
+    {
+        if (isset($sets[$key])) {
+            return '' == $sets[$key];
+        } else {
+            return '' == $cache[$key];
+        }
+    }
+
+    public function update(array $form): self
+    {
+        $cache  = $this->cache();
+        $fields = $this->c->dbMap->providers;
+
+        foreach ($form as $driver => $sets) {
+            if (! isset($this->drivers[$driver], $cache[$driver])) {
+                continue;
+            }
+
+            $set = $vars = [];
+
+            if (
+                (
+                    ! empty($sets['pr_allow'])
+                    || ! empty($cache[$driver]['pr_allow'])
+                )
+                && (
+                    $this->emptyField('pr_cl_id', $sets, $cache[$driver])
+                    || $this->emptyField('pr_cl_sec', $sets, $cache[$driver])
+                )
+            ) {
+                $sets['pr_allow'] = 0;
+            }
+
+            foreach ($sets as $name => $value) {
+                if (
+                    ! isset($fields[$name])
+                    || $cache[$driver][$name] == $value
+                ) {
+                    continue;
+                }
+
+                $vars[] = $value;
+                $set[]  = $name . '=?' . $fields[$name];
+            }
+
+            if (empty($set)) {
+                continue;
+            }
+
+            $vars[] = $driver;
+            $set    = \implode(', ', $set);
+            $query  = "UPDATE ::providers
+                SET {$set}
+                WHERE pr_name=?s";
+
+            $this->c->DB->exec($query, $vars);
+        }
+
+        if (true !== $this->c->Cache->delete(self::CACHE_KEY)) {
+            throw new RuntimeException('Unable to delete cache - ' . self::CACHE_KEY);
+        }
+
+        return $this;
+    }
+}

+ 8 - 0
app/config/main.dist.php

@@ -163,6 +163,12 @@ return [
         'subscriptions' => \ForkBB\Models\Subscription\Subscription::class,
         'subscriptions' => \ForkBB\Models\Subscription\Subscription::class,
         'bbcode'        => '@BBCodeListModel:init',
         'bbcode'        => '@BBCodeListModel:init',
         'pms'           => \ForkBB\Models\PM\PM::class,
         'pms'           => \ForkBB\Models\PM\PM::class,
+        'providers'     => [
+            'class'   => \ForkBB\Models\Provider\Providers::class,
+            'drivers' => [
+                'github' => \ForkBB\Models\Provider\Driver\GitHub::class,
+            ],
+        ],
 
 
         'Csrf' => [
         'Csrf' => [
             'class'  => \ForkBB\Core\Csrf::class,
             'class'  => \ForkBB\Core\Csrf::class,
@@ -333,6 +339,7 @@ return [
         'Userlist'           => \ForkBB\Models\Pages\Userlist::class,
         'Userlist'           => \ForkBB\Models\Pages\Userlist::class,
         'Search'             => \ForkBB\Models\Pages\Search::class,
         'Search'             => \ForkBB\Models\Pages\Search::class,
         'Register'           => \ForkBB\Models\Pages\Register::class,
         'Register'           => \ForkBB\Models\Pages\Register::class,
+        'RegLog'             => \ForkBB\Models\Pages\RegLog::class,
         'Redirect'           => \ForkBB\Models\Pages\Redirect::class,
         'Redirect'           => \ForkBB\Models\Pages\Redirect::class,
         'Maintenance'        => \ForkBB\Models\Pages\Maintenance::class,
         'Maintenance'        => \ForkBB\Models\Pages\Maintenance::class,
         'Ban'                => \ForkBB\Models\Pages\Ban::class,
         'Ban'                => \ForkBB\Models\Pages\Ban::class,
@@ -360,6 +367,7 @@ return [
         'AdminIndex'         => \ForkBB\Models\Pages\Admin\Index::class,
         'AdminIndex'         => \ForkBB\Models\Pages\Admin\Index::class,
         'AdminStatistics'    => \ForkBB\Models\Pages\Admin\Statistics::class,
         'AdminStatistics'    => \ForkBB\Models\Pages\Admin\Statistics::class,
         'AdminOptions'       => \ForkBB\Models\Pages\Admin\Options::class,
         'AdminOptions'       => \ForkBB\Models\Pages\Admin\Options::class,
+        'AdminProviders'     => \ForkBB\Models\Pages\Admin\Providers::class,
         'AdminCategories'    => \ForkBB\Models\Pages\Admin\Categories::class,
         'AdminCategories'    => \ForkBB\Models\Pages\Admin\Categories::class,
         'AdminForums'        => \ForkBB\Models\Pages\Admin\Forums::class,
         'AdminForums'        => \ForkBB\Models\Pages\Admin\Forums::class,
         'AdminGroups'        => \ForkBB\Models\Pages\Admin\Groups::class,
         'AdminGroups'        => \ForkBB\Models\Pages\Admin\Groups::class,

+ 9 - 0
app/lang/en/admin_options.po

@@ -425,3 +425,12 @@ msgstr "Allow private messages"
 
 
 msgid "Allow PM help"
 msgid "Allow PM help"
 msgstr "Settings for individual groups are available in <a href="%2$s">%1$s</a>."
 msgstr "Settings for individual groups are available in <a href="%2$s">%1$s</a>."
+
+msgid "Allow oauth label"
+msgstr "OAuth-authorization"
+
+msgid "Allow oauth help"
+msgstr "Allow users to login/register on this bulletin board using accounts on third-party resources."
+
+msgid "Configure providers"
+msgstr "Configure providers"

+ 70 - 0
app/lang/en/admin_providers.po

@@ -0,0 +1,70 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
+msgid "OAuth authorization disabled"
+msgstr "OAuth authorization <a href=\"%s\">disabled</a>."
+
+msgid "Providers"
+msgstr "Providers"
+
+msgid "Provider label"
+msgstr "Provider"
+
+msgid "Position label"
+msgstr "Position"
+
+msgid "Allow label"
+msgstr "Allow"
+
+msgid "Providers updated redirect"
+msgstr "Provider table updated."
+
+msgid "Provider updated redirect"
+msgstr "Provider data updated."
+
+msgid "github"
+msgstr "GitHub"
+
+msgid "Callback label"
+msgstr "Callback URI"
+
+msgid "Callback help"
+msgstr "The URI that the user is returned to after they have granted or denied access to the application (corresponds to the redirect_uri of the OAuth protocol). It must be specified when registering your application on the provider's server."
+
+msgid "ID label"
+msgstr "Client ID"
+
+msgid "ID help"
+msgstr "Identifier that you can get when registering your application on the provider's server."
+
+msgid "Secret label"
+msgstr "Client secret"
+
+msgid "Secret help"
+msgstr "The secret (password) that you can get when registering your application on the provider's server."
+
+msgid "Change data help"
+msgstr "Enable this checkbox if you want to change the data"
+
+msgid "Sign in with %s"
+msgstr "Sign in with %s"
+
+msgid "Provider response: %s"
+msgstr "Provider response: %s."
+
+msgid "State error"
+msgstr "Error in state parameter."
+
+msgid "Error token"
+msgstr "Error request access token"

+ 10 - 1
app/lang/ru/admin_options.po

@@ -280,7 +280,7 @@ msgid "SMTP password label"
 msgstr "SMTP password"
 msgstr "SMTP password"
 
 
 msgid "SMTP change password help"
 msgid "SMTP change password help"
-msgstr "Установите галку, если хотите поменять или удалить пароль"
+msgstr "Установите флажок, если хотите поменять или удалить пароль"
 
 
 msgid "SMTP password help"
 msgid "SMTP password help"
 msgstr "Пароль для сервера SMTP. Заполняйте только если сервер этого требует."
 msgstr "Пароль для сервера SMTP. Заполняйте только если сервер этого требует."
@@ -425,3 +425,12 @@ msgstr "Разрешить личные сообщения"
 
 
 msgid "Allow PM help"
 msgid "Allow PM help"
 msgstr "Настройки для отдельных групп доступны в <a href="%2$s">%1$s</a>."
 msgstr "Настройки для отдельных групп доступны в <a href="%2$s">%1$s</a>."
+
+msgid "Allow oauth label"
+msgstr "OAuth-авторизация"
+
+msgid "Allow oauth help"
+msgstr "Разрешить пользователям вход/регистрацию на форуме с помощью аккаунтов на сторонних ресурсах."
+
+msgid "Configure providers"
+msgstr "Настроить провайдеров"

+ 70 - 0
app/lang/ru/admin_providers.po

@@ -0,0 +1,70 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+
+msgid "OAuth authorization disabled"
+msgstr "OAuth-авторизация <a href=\"%s\">выключена</a>."
+
+msgid "Providers"
+msgstr "Провайдеры"
+
+msgid "Provider label"
+msgstr "Провайдер"
+
+msgid "Position label"
+msgstr "Позиция"
+
+msgid "Allow label"
+msgstr "Вкл."
+
+msgid "Providers updated redirect"
+msgstr "Таблица провайдеров обновлена."
+
+msgid "Provider updated redirect"
+msgstr "Данные провайдера обновлены."
+
+msgid "github"
+msgstr "GitHub"
+
+msgid "Callback label"
+msgstr "URI перенаправления"
+
+msgid "Callback help"
+msgstr "Адрес, на который пользователь возращается после того, как он разрешил или отказал приложению в доступе (соответствует redirect_uri протокола OAuth). Его нужно указать при регистрации своего приложения на сервере провайдера."
+
+msgid "ID label"
+msgstr "Идентификатор клиента"
+
+msgid "ID help"
+msgstr "Идентификатор (ID), который можно получить при регистрации своего приложения на сервере провайдера."
+
+msgid "Secret label"
+msgstr "Секрет клиента"
+
+msgid "Secret help"
+msgstr "Секрет (пароль), который можно получить при регистрации своего приложения на сервере провайдера."
+
+msgid "Change data help"
+msgstr "Установите флажок, если хотите изменить данные"
+
+msgid "Sign in with %s"
+msgstr "Войти с помощью %s"
+
+msgid "Provider response: %s"
+msgstr "Ответ провайдера: %s."
+
+msgid "State error"
+msgstr "Ошибка в параметре state."
+
+msgid "Error token"
+msgstr "Ошибка запроса токена доступа"

+ 7 - 0
app/templates/login.forkbb.php

@@ -14,3 +14,10 @@
     @endif
     @endif
     </section>
     </section>
 @endif
 @endif
+@if ($form = $p->formOAuth)
+    <div id="fork-oauth" class="f-main">
+      <div class="f-fdiv f-lrdiv">
+    @include ('layouts/form')
+      </div>
+    </div>
+@endif

+ 7 - 0
app/templates/register.forkbb.php

@@ -7,3 +7,10 @@
       </div>
       </div>
     </section>
     </section>
 @endif
 @endif
+@if ($form = $p->formOAuth)
+    <div id="fork-oauth" class="f-main">
+      <div class="f-fdiv f-lrdiv">
+    @include ('layouts/form')
+      </div>
+    </div>
+@endif

+ 33 - 0
public/style/ForkBB/admin.css

@@ -208,7 +208,9 @@
 }
 }
 
 
 #forka .f-fs-inline dl {
 #forka .f-fs-inline dl {
+  display: flex;
   flex-direction: column;
   flex-direction: column;
+  justify-content: space-between;
 }
 }
 
 
 #forka .f-fs-inline dt {
 #forka .f-fs-inline dt {
@@ -1026,3 +1028,34 @@
     flex-wrap: wrap;
     flex-wrap: wrap;
   }
   }
 }
 }
+
+/****************************************/
+/* Админка/Провайдеры                   */
+/****************************************/
+#forka .f-field-provider.f-field-name {
+  width: calc(100% - 8rem);
+}
+
+#forka .f-field-provider.f-field-position {
+  width: 5rem;
+}
+
+#forka .f-field-provider.f-field-allow {
+  width: 3rem;
+  overflow: hidden;
+  text-align: center;
+}
+
+#forka .f-field-provider.f-field-name .f-ybtn {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+#forka .f-field-provider.f-field-allow .f-ychk {
+  margin: 0;
+}
+
+#forka .f-field-provider.f-field-allow .f-flblch {
+  height: 2rem;
+}