<?php
namespace App\Controller;
use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\{JsonResponse, Request, Response, RedirectResponse, Cookie};
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use J4k\OAuth2\Client\Provider\Vkontakte;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Symfony\Component\HttpClient\HttpClient;
class AuthController extends BaseController
{
private const DEFAULT_AVATAR = '/assets/images/default-avatar.png';
/**
* @Route("/auth/register", name="auth_register", methods={"POST"})
*/
public function register(Request $request, Connection $db): Response
{
$payload = json_decode($request->getContent(), true) ?? [];
$login = trim($payload['login'] ?? '');
$email = trim($payload['email'] ?? '');
$password = $payload['password'] ?? '';
// $captcha = $payload['captcha'] ?? '';
if (!$login || !$email || !$password) {
return new JsonResponse(['error' => 'Все поля обязательны'], 422);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return new JsonResponse(['error' => 'Некорректный e‑mail'], 422);
}
if (mb_strlen($password) < 6) {
return new JsonResponse(['error' => 'Пароль короче 6 символов'], 422);
}
// reCAPTCHA temporarily disabled for local registration testing.
// $http = HttpClient::create();
// $resp = $http->request('POST', 'https://www.google.com/recaptcha/api/siteverify', [
// 'body' => [
// 'secret' => $_ENV['RECAPTCHA_SECRET_KEY'],
// 'response' => $captcha,
// 'remoteip' => $request->getClientIp(),
// ],
// ])->toArray(false);
//
// if (!$resp['success'] ?? true) {
// return new JsonResponse(['error' => 'Проверка «Я не робот» не пройдена'], 400);
// }
$exists = $db->fetchOne(
'SELECT 1 FROM users WHERE login = ? OR email = ?',
[$login, $email]
);
if ($exists) {
return new JsonResponse(['error' => 'Такой логин или e‑mail уже зарегистрирован'], 409);
}
$plainToken = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $plainToken);
$authToken = md5(bin2hex(random_bytes(32)));
$db->insert('users', [
'login' => $login,
'email' => $email,
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'balance' => 0.00,
'hash' => 'local:' . $login,
'img' => self::DEFAULT_AVATAR,
'auth_token' => $authToken,
'auth_token_hash' => $tokenHash,
'ip' => $request->getClientIp(),
'ref_id' => $request->getSession()->get('ref_id', 0),
]);
$request->getSession()->migrate(true);
$request->getSession()->set('hash', 'local:' . $login);
$response = new JsonResponse(['ok' => true]);
$response->headers->setCookie(Cookie::create(
'auth_token',
$plainToken,
new \DateTime('+7 days'),
'/',
null,
$request->isSecure(),
true,
false,
'Strict'
));
return $response;
}
/**
* @Route("/auth/login", name="auth_login", methods={"POST"})
*/
public function login(Request $request, Connection $db): Response
{
$payload = json_decode($request->getContent(), true) ?? [];
$login = trim($payload['login'] ?? '');
$password = $payload['password'] ?? '';
$user = $db->fetchAssociative(
'SELECT * FROM users WHERE login = ?',
[$login]
);
if (!$user || !password_verify($password, $user['password_hash'])) {
return new JsonResponse(['error' => 'Неверный логин или пароль'], 401);
}
$plainToken = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $plainToken);
$db->update('users', ['auth_token_hash' => $tokenHash], ['id' => $user['id']]);
$request->getSession()->migrate(true);
$request->getSession()->set('hash', $user['hash']);
$response = new JsonResponse(['ok' => true]);
$response->headers->setCookie(Cookie::create(
'auth_token',
$plainToken,
new \DateTime('+7 days'),
'/',
null,
$request->isSecure(),
true,
false,
'Strict'
));
return $response;
}
/**
* @Route("/auth/vk", name="vk_connect")
*/
public function connect(Request $request): RedirectResponse
{
$provider = $this->getProvider();
$authUrl = $provider->getAuthorizationUrl();
$request->getSession()->set('oauth2state', $provider->getState());
return new RedirectResponse($authUrl);
}
/**
* @Route("/auth/tg", name="tg_connect", methods={"GET"})
*/
public function tgConnect(Request $request): Response
{
$back = $request->query->get('back', $request->headers->get('referer', '/'));
return $this->render('auth/tg_connect.html.twig', [
'bot_username' => $_ENV['TG_BOT_USERNAME'],
'back_url' => $back,
]);
}
/**
* @Route("/auth/vk/callback", name="vk_check")
*/
public function check(Request $request, Connection $db): Response
{
$session = $request->getSession();
if (!$request->query->has('code') ||
$request->query->get('state') !== $session->get('oauth2state')) {
return new Response('Invalid state', 400);
}
try {
$provider = $this->getProvider();
$token = $provider->getAccessToken('authorization_code', [
'code' => $request->query->get('code'),
]);
$vkData = $provider->getResourceOwner($token)->toArray();
$userId = $vkData['id'] ?? null;
if ($userId) {
$response = file_get_contents('https://api.vk.com/method/users.get?' . http_build_query([
'user_ids' => $userId,
'fields' => 'photo_max,photo_200_orig,sex',
'access_token' => $token->getToken(),
'v' => '5.199',
]));
$extra = json_decode($response, true);
if (!empty($extra['response'][0])) {
$vkData = array_merge($vkData, $extra['response'][0]);
}
}
$vkid = 'http://vk.com/id'.$vkData['id'];
$user = $db->fetchAssociative('SELECT * FROM users WHERE hash = ?', [$vkid]);
/* ротация токена */
$plainToken = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $plainToken); // то, что идёт в БД
/* для слотов */
$authToken = md5(bin2hex(random_bytes(32)));
if (!$user) {
$refId = $session->get('ref_id');
$db->insert('users', [
'login' => trim(($vkData['first_name'] ?? '').' '.($vkData['last_name'] ?? '')),
'balance' => 0.00,
'hash' => $vkid,
'img' => ($vkData['photo_max'] ?? '') ?: self::DEFAULT_AVATAR,
'auth_token' => $authToken,
'auth_token_hash'=> $tokenHash,
'ip' => $request->getClientIp(),
'ref_id' => $refId ?: 0,
]);
if ($refId) {
$db->executeStatement('UPDATE users SET refs = refs + 1 WHERE id = ?', [$refId]);
$session->remove('ref_id');
}
} else {
$db->update('users', ['auth_token_hash' => $tokenHash], ['id' => $user['id']]);
}
$session->migrate(true); // фиксация сессии
$session->set('hash', $vkid);
$secure = $request->isSecure();
$resp = $this->redirectToRoute('main_page');
$resp->headers->setCookie(
Cookie::create(
'auth_token',
$plainToken, // не храним
new \DateTime('+7 days'),
'/',
null,
$secure,
true, // HttpOnly
false,
'Strict'
)
);
return $resp;
} catch (IdentityProviderException $e) {
return new Response(
'Не удалось войти. Пожалуйста, попробуйте еще раз'
);
}
}
/**
* @Route("/auth/tg/callback", name="tg_check", methods={"GET"})
*/
public function tgCheck(Request $request, Connection $db): Response
{
$data = $request->query->all();
if (!$this->isValidTelegramAuth($data)) {
return new Response('Неверные данные авторизации Telegram', 400);
}
$tgId = (string) $data['id'];
$fname = trim($data['first_name'] ?? '');
$lname = trim($data['last_name'] ?? '');
$login = $fname . ($lname ? ' ' . $lname : '');
$identity = [
'hash' => 'tg://user?id=' . $tgId,
'login' => $login ?: ('tg_user_' . $tgId),
'img' => filter_var($data['photo_url'] ?? '', FILTER_VALIDATE_URL) ?: self::DEFAULT_AVATAR,
];
return $this->finishLoginTg($identity, $db, $request, $tgId);
}
/**
* @Route("/logout", name="app_logout")
*/
public function logout(Request $request, Connection $db): Response
{
$session = $request->getSession();
$response = new RedirectResponse('/');
$hash = $session->get('hash');
if ($hash) {
$db->update('users', ['auth_token_hash' => null], ['hash' => $hash]);
}
$response->headers->setCookie(
new Cookie(
'auth_token',
'',
time() - 3600,
'/',
null,
true,
true,
false,
Cookie::SAMESITE_STRICT
)
);
$session->remove('hash');
$session->invalidate();
return $response;
}
private function getProvider(): Vkontakte
{
return new Vkontakte([
'clientId' => $_ENV['VK_CLIENT_ID'],
'clientSecret' => $_ENV['VK_CLIENT_SECRET'],
'redirectUri' => $this->generateUrl('vk_check', [], 0),
]);
}
private function isValidTelegramAuth(array $data): bool
{
foreach (['id', 'hash', 'auth_date'] as $k) {
if (!isset($data[$k])) {
return false;
}
}
if (abs(time() - (int)$data['auth_date']) > 60) {
return false;
}
$checkHash = $data['hash'];
unset($data['hash']);
ksort($data);
$dataCheckString = '';
foreach ($data as $k => $v) {
$dataCheckString .= $k . '=' . $v . "\n";
}
$dataCheckString = rtrim($dataCheckString, "\n");
$secretKey = hash('sha256', $_ENV['TG_BOT_TOKEN'], true);
$calcHash = hash_hmac('sha256', $dataCheckString, $secretKey);
return hash_equals($calcHash, $checkHash);
}
private function finishLoginTg(
array $identity,
Connection $db,
Request $request,
string $tgId
): Response {
$session = $request->getSession();
$plainToken = bin2hex(random_bytes(32)); // то, что кладём в cookie
$tokenHash = hash('sha256', $plainToken); // храним в БД
$authToken = md5(bin2hex(random_bytes(32)));
$user = $db->fetchAssociative(
'SELECT * FROM users WHERE hash = ?',
[$identity['hash']]
);
if (!$user) {
$refId = $session->get('ref_id');
$db->insert('users', [
'login' => $identity['login'],
'balance' => 0.00,
'hash' => $identity['hash'],
'img' => $identity['img'],
'auth_token' => $authToken,
'auth_token_hash' => $tokenHash,
'ip' => $request->getClientIp(),
'ref_id' => $refId ?: 0,
'tg_id' => $tgId
]);
if ($refId) {
$db->executeStatement(
'UPDATE users SET refs = refs + 1, refbal = refbal + 50 WHERE id = ?',
[$refId]
);
$session->remove('ref_id');
}
} else {
$db->update(
'users',
['auth_token_hash' => $tokenHash],
['id' => $user['id']]
);
}
$session->migrate(true);
$session->set('hash', $identity['hash']);
$resp = $this->redirectToRoute('main_page');
$resp->headers->setCookie(
Cookie::create(
'auth_token',
$plainToken,
new \DateTime('+7 days'),
'/',
null,
$request->isSecure(),
true, // HttpOnly
false,
'Strict'
)
);
return $resp;
}
}