src/Controller/AuthController.php line 318

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use Doctrine\DBAL\Connection;
  4. use Symfony\Component\HttpFoundation\{JsonResponseRequestResponseRedirectResponseCookie};
  5. use Symfony\Component\Routing\Annotation\Route;
  6. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  7. use Symfony\Component\Security\Core\User\User;
  8. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  9. use Symfony\Component\Security\Core\User\UserInterface;
  10. use J4k\OAuth2\Client\Provider\Vkontakte;
  11. use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
  12. use Symfony\Component\HttpClient\HttpClient;
  13. class AuthController extends BaseController
  14. {
  15.     private const DEFAULT_AVATAR '/assets/images/default-avatar.png';
  16.     
  17.     /**
  18.      * @Route("/auth/register", name="auth_register", methods={"POST"})
  19.      */
  20.     public function register(Request $requestConnection $db): Response
  21.     {
  22.         $payload json_decode($request->getContent(), true) ?? [];
  23.         $login    trim($payload['login']    ?? '');
  24.         $email    trim($payload['email']    ?? '');
  25.         $password =        $payload['password'] ?? '';
  26.         // $captcha  =        $payload['captcha']  ?? '';
  27.         if (!$login || !$email || !$password) {
  28.             return new JsonResponse(['error' => 'Все поля обязательны'], 422);
  29.         }
  30.         if (!filter_var($emailFILTER_VALIDATE_EMAIL)) {
  31.             return new JsonResponse(['error' => 'Некорректный e‑mail'], 422);
  32.         }
  33.         if (mb_strlen($password) < 6) {
  34.             return new JsonResponse(['error' => 'Пароль короче 6 символов'], 422);
  35.         }
  36.         // reCAPTCHA temporarily disabled for local registration testing.
  37.         // $http = HttpClient::create();
  38.         // $resp = $http->request('POST', 'https://www.google.com/recaptcha/api/siteverify', [
  39.         //     'body' => [
  40.         //         'secret'   => $_ENV['RECAPTCHA_SECRET_KEY'],
  41.         //         'response' => $captcha,
  42.         //         'remoteip' => $request->getClientIp(),
  43.         //     ],
  44.         // ])->toArray(false);
  45.         //
  46.         // if (!$resp['success'] ?? true) {
  47.         //     return new JsonResponse(['error' => 'Проверка «Я не робот» не пройдена'], 400);
  48.         // }
  49.         $exists $db->fetchOne(
  50.             'SELECT 1 FROM users WHERE login = ? OR email = ?',
  51.             [$login$email]
  52.         );
  53.         if ($exists) {
  54.             return new JsonResponse(['error' => 'Такой логин или e‑mail уже зарегистрирован'], 409);
  55.         }
  56.         $plainToken bin2hex(random_bytes(32));
  57.         $tokenHash  hash('sha256'$plainToken);
  58.         $authToken  md5(bin2hex(random_bytes(32)));
  59.         $db->insert('users', [
  60.             'login'           => $login,
  61.             'email'           => $email,
  62.             'password_hash'   => password_hash($passwordPASSWORD_DEFAULT),
  63.             'balance'         => 0.00,
  64.             'hash'            => 'local:' $login,
  65.             'img'             => self::DEFAULT_AVATAR,
  66.             'auth_token'      => $authToken,
  67.             'auth_token_hash' => $tokenHash,
  68.             'ip'              => $request->getClientIp(),
  69.             'ref_id'          => $request->getSession()->get('ref_id'0),
  70.         ]);
  71.         $request->getSession()->migrate(true);
  72.         $request->getSession()->set('hash''local:' $login);
  73.         $response = new JsonResponse(['ok' => true]);
  74.         $response->headers->setCookie(Cookie::create(
  75.             'auth_token',
  76.             $plainToken,
  77.             new \DateTime('+7 days'),
  78.             '/',
  79.             null,
  80.             $request->isSecure(),
  81.             true,
  82.             false,
  83.             'Strict'
  84.         ));
  85.         return $response;
  86.     }
  87.     /**
  88.      * @Route("/auth/login", name="auth_login", methods={"POST"})
  89.      */
  90.     public function login(Request $requestConnection $db): Response
  91.     {
  92.         $payload  json_decode($request->getContent(), true) ?? [];
  93.         $login    trim($payload['login'] ?? '');
  94.         $password =       $payload['password'] ?? '';
  95.         $user $db->fetchAssociative(
  96.             'SELECT * FROM users WHERE login = ?',
  97.             [$login]
  98.         );
  99.         if (!$user || !password_verify($password$user['password_hash'])) {
  100.             return new JsonResponse(['error' => 'Неверный логин или пароль'], 401);
  101.         }
  102.         $plainToken bin2hex(random_bytes(32));
  103.         $tokenHash  hash('sha256'$plainToken);
  104.         $db->update('users', ['auth_token_hash' => $tokenHash], ['id' => $user['id']]);
  105.         $request->getSession()->migrate(true);
  106.         $request->getSession()->set('hash'$user['hash']);
  107.         $response = new JsonResponse(['ok' => true]);
  108.         $response->headers->setCookie(Cookie::create(
  109.             'auth_token',
  110.             $plainToken,
  111.             new \DateTime('+7 days'),
  112.             '/',
  113.             null,
  114.             $request->isSecure(),
  115.             true,
  116.             false,
  117.             'Strict'
  118.         ));
  119.         return $response;
  120.     }
  121.     
  122.     /**
  123.      * @Route("/auth/vk", name="vk_connect")
  124.      */
  125.     public function connect(Request $request): RedirectResponse
  126.     {
  127.         $provider $this->getProvider();
  128.         $authUrl $provider->getAuthorizationUrl();
  129.         $request->getSession()->set('oauth2state'$provider->getState());
  130.         return new RedirectResponse($authUrl);
  131.     }
  132.     
  133.     /**
  134.      * @Route("/auth/tg", name="tg_connect", methods={"GET"})
  135.      */
  136.     public function tgConnect(Request $request): Response
  137.     {
  138.         $back $request->query->get('back'$request->headers->get('referer''/'));
  139.         return $this->render('auth/tg_connect.html.twig', [
  140.             'bot_username' => $_ENV['TG_BOT_USERNAME'],
  141.             'back_url'     => $back,
  142.         ]);
  143.     }
  144.     /**
  145.      * @Route("/auth/vk/callback", name="vk_check")
  146.      */
  147.      public function check(Request $requestConnection $db): Response
  148.     {
  149.         $session $request->getSession();
  150.         if (!$request->query->has('code') ||
  151.             $request->query->get('state') !== $session->get('oauth2state')) {
  152.             return new Response('Invalid state'400);
  153.         }
  154.         try {
  155.             $provider   $this->getProvider();
  156.             $token      $provider->getAccessToken('authorization_code', [
  157.                 'code' => $request->query->get('code'),
  158.             ]);
  159.             $vkData     $provider->getResourceOwner($token)->toArray();
  160.             
  161.             $userId $vkData['id'] ?? null;
  162.             if ($userId) {
  163.                 $response file_get_contents('https://api.vk.com/method/users.get?' http_build_query([
  164.                     'user_ids' => $userId,
  165.                     'fields' => 'photo_max,photo_200_orig,sex',
  166.                     'access_token' => $token->getToken(),
  167.                     'v' => '5.199',
  168.                 ]));
  169.                 $extra json_decode($responsetrue);
  170.                 if (!empty($extra['response'][0])) {
  171.                     $vkData array_merge($vkData$extra['response'][0]);
  172.                 }
  173.             }
  174.             
  175.             $vkid   'http://vk.com/id'.$vkData['id'];
  176.             $user   $db->fetchAssociative('SELECT * FROM users WHERE hash = ?', [$vkid]);
  177.             /* ротация токена */
  178.             $plainToken bin2hex(random_bytes(32));                   
  179.             $tokenHash  hash('sha256'$plainToken);                 // то, что идёт в БД
  180.             
  181.             /* для слотов */
  182.             $authToken md5(bin2hex(random_bytes(32)));
  183.             
  184.             if (!$user) {             
  185.                 $refId $session->get('ref_id');
  186.                 $db->insert('users', [
  187.                     'login'          => trim(($vkData['first_name'] ?? '').' '.($vkData['last_name'] ?? '')),
  188.                     'balance'        => 0.00,
  189.                     'hash'           => $vkid,
  190.                     'img'            => ($vkData['photo_max'] ?? '') ?: self::DEFAULT_AVATAR,
  191.                     'auth_token'     => $authToken,
  192.                     'auth_token_hash'=> $tokenHash,
  193.                     'ip'             => $request->getClientIp(),
  194.                     'ref_id'          => $refId ?: 0,
  195.                 ]);
  196.                 
  197.                 if ($refId) {
  198.                     $db->executeStatement('UPDATE users SET refs = refs + 1 WHERE id = ?', [$refId]);
  199.                     $session->remove('ref_id'); 
  200.                 }
  201.             } else {                                                   
  202.                 $db->update('users', ['auth_token_hash' => $tokenHash], ['id' => $user['id']]);
  203.             }
  204.             $session->migrate(true);                                   // фиксация сессии
  205.             $session->set('hash'$vkid);
  206.             $secure $request->isSecure();                            
  207.             $resp   $this->redirectToRoute('main_page');
  208.             $resp->headers->setCookie(
  209.                 Cookie::create(
  210.                     'auth_token',
  211.                     $plainToken,                                       // не храним 
  212.                     new \DateTime('+7 days'),
  213.                     '/',
  214.                     null,
  215.                     $secure,
  216.                     true,                                              // HttpOnly
  217.                     false,
  218.                     'Strict'
  219.                 )
  220.             );
  221.             return $resp;
  222.         } catch (IdentityProviderException $e) {
  223.             return new Response(
  224.                 'Не удалось войти. Пожалуйста, попробуйте еще раз'
  225.             );
  226.         }
  227.     }
  228.     
  229.     /**
  230.      *  @Route("/auth/tg/callback", name="tg_check", methods={"GET"})
  231.      */
  232.     public function tgCheck(Request $requestConnection $db): Response
  233.     {
  234.         $data $request->query->all();
  235.         if (!$this->isValidTelegramAuth($data)) {
  236.             return new Response('Неверные данные авторизации Telegram'400);
  237.         }
  238.         $tgId  = (string) $data['id'];
  239.         $fname trim($data['first_name'] ?? '');
  240.         $lname trim($data['last_name'] ?? '');
  241.         $login $fname . ($lname ' ' $lname '');
  242.         $identity = [
  243.             'hash'  => 'tg://user?id=' $tgId,
  244.             'login' => $login ?: ('tg_user_' $tgId),
  245.             'img'   => filter_var($data['photo_url'] ?? ''FILTER_VALIDATE_URL) ?: self::DEFAULT_AVATAR,
  246.         ];
  247.         return $this->finishLoginTg($identity$db$request$tgId);
  248.     }
  249.     
  250.     /**
  251.      * @Route("/logout", name="app_logout")
  252.      */
  253.     public function logout(Request $requestConnection $db): Response
  254.     {  
  255.         $session $request->getSession();
  256.         $response = new RedirectResponse('/');
  257.         $hash $session->get('hash');
  258.         if ($hash) {
  259.             $db->update('users', ['auth_token_hash' => null], ['hash' => $hash]);
  260.         }
  261.         $response->headers->setCookie(
  262.             new Cookie(
  263.                 'auth_token',
  264.                 '',
  265.                 time() - 3600,
  266.                 '/',
  267.                 null,
  268.                 true,
  269.                 true,
  270.                 false,
  271.                 Cookie::SAMESITE_STRICT
  272.             )
  273.         );
  274.         $session->remove('hash');
  275.         $session->invalidate();
  276.         return $response;
  277.     }
  278.     
  279.     private function getProvider(): Vkontakte
  280.     {
  281.         return new Vkontakte([
  282.             'clientId'     => $_ENV['VK_CLIENT_ID'],
  283.             'clientSecret' => $_ENV['VK_CLIENT_SECRET'],
  284.             'redirectUri'  => $this->generateUrl('vk_check', [], 0),
  285.         ]);
  286.     }
  287.     
  288.     private function isValidTelegramAuth(array $data): bool
  289.     {
  290.         foreach (['id''hash''auth_date'] as $k) {
  291.             if (!isset($data[$k])) {
  292.                 return false;
  293.             }
  294.         }
  295.         if (abs(time() - (int)$data['auth_date']) > 60) {
  296.             return false;
  297.         }
  298.         $checkHash $data['hash'];
  299.         unset($data['hash']);
  300.         ksort($data);
  301.         $dataCheckString '';
  302.         foreach ($data as $k => $v) {
  303.             $dataCheckString .= $k '=' $v "\n";
  304.         }
  305.         $dataCheckString rtrim($dataCheckString"\n");
  306.         $secretKey hash('sha256'$_ENV['TG_BOT_TOKEN'], true);
  307.         $calcHash hash_hmac('sha256'$dataCheckString$secretKey);
  308.         return hash_equals($calcHash$checkHash);
  309.     }
  310.     
  311.     private function finishLoginTg(
  312.         array      $identity,    
  313.         Connection $db,
  314.         Request    $request,
  315.         string        $tgId
  316.     ): Response {
  317.         $session    $request->getSession();
  318.         $plainToken bin2hex(random_bytes(32));       // то, что кладём в cookie
  319.         $tokenHash  hash('sha256'$plainToken);     // храним в БД
  320.         $authToken  md5(bin2hex(random_bytes(32)));  
  321.         $user $db->fetchAssociative(
  322.             'SELECT * FROM users WHERE hash = ?',
  323.             [$identity['hash']]
  324.         );
  325.         if (!$user) {
  326.             $refId $session->get('ref_id');
  327.             $db->insert('users', [
  328.                 'login'           => $identity['login'],
  329.                 'balance'         => 0.00,
  330.                 'hash'            => $identity['hash'],
  331.                 'img'             => $identity['img'],
  332.                 'auth_token'      => $authToken,
  333.                 'auth_token_hash' => $tokenHash,
  334.                 'ip'              => $request->getClientIp(),
  335.                 'ref_id'          => $refId ?: 0,
  336.                 'tg_id'              => $tgId
  337.             ]);
  338.             if ($refId) {
  339.                 $db->executeStatement(
  340.                     'UPDATE users SET refs = refs + 1, refbal = refbal + 50 WHERE id = ?',
  341.                     [$refId]
  342.                 );
  343.                 $session->remove('ref_id');
  344.             }
  345.         } else {
  346.             $db->update(
  347.                 'users',
  348.                 ['auth_token_hash' => $tokenHash],
  349.                 ['id' => $user['id']]
  350.             );
  351.         }
  352.         $session->migrate(true);
  353.         $session->set('hash'$identity['hash']);
  354.         $resp $this->redirectToRoute('main_page');
  355.         $resp->headers->setCookie(
  356.             Cookie::create(
  357.                 'auth_token',
  358.                 $plainToken,
  359.                 new \DateTime('+7 days'),
  360.                 '/',
  361.                 null,
  362.                 $request->isSecure(),
  363.                 true,                   // HttpOnly
  364.                 false,
  365.                 'Strict'
  366.             )
  367.         );
  368.         return $resp;
  369.     }
  370. }