<?php
namespace App\Controller;
use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class AuthController extends BaseController
{
/**
* @Route("/auth/register", name="auth_register", methods={"POST"})
*/
public function register(Request $request, Connection $db): Response
{
$this->ensureSchema($db);
$payload = $this->payload($request);
$login = trim((string) ($payload['login'] ?? ''));
$email = trim((string) ($payload['email'] ?? ''));
$password = (string) ($payload['password'] ?? '');
$passwordRepeat = (string) ($payload['passwordRepeat'] ?? $payload['password_repeat'] ?? '');
if ($login === '' || $email === '' || $password === '') {
return $this->error('Все поля обязательны', 422);
}
if (mb_strlen($login) < 3 || mb_strlen($login) > 50) {
return $this->error('Логин должен быть от 3 до 50 символов', 422);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->error('Некорректный e-mail', 422);
}
if (mb_strlen($password) < 6) {
return $this->error('Пароль короче 6 символов', 422);
}
if ($passwordRepeat !== '' && $password !== $passwordRepeat) {
return $this->error('Пароли не совпадают', 422);
}
$exists = $db->fetchOne(
'SELECT 1 FROM users WHERE login = ? OR email = ? LIMIT 1',
[$login, $email]
);
if ($exists) {
return $this->error('Такой логин или e-mail уже зарегистрирован', 409);
}
$plainToken = bin2hex(random_bytes(32));
$userHash = 'local:' . $login;
$db->insert('users', [
'login' => $login,
'email' => $email,
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'balance' => 0,
'hash' => $userHash,
'img' => '/assets/profile.svg',
'auth_token_hash' => hash('sha256', $plainToken),
'ip' => (string) $request->getClientIp(),
'ref_id' => 0,
'refs' => 0,
'created_at' => (new \DateTimeImmutable())->format('Y-m-d H:i:s'),
]);
$request->getSession()->migrate(true);
$request->getSession()->set('hash', $userHash);
return $this->tokenResponse($plainToken, [
'ok' => true,
'user' => [
'login' => $login,
'email' => $email,
'balance' => 0,
],
], $request);
}
/**
* @Route("/auth/login", name="auth_login", methods={"POST"})
*/
public function login(Request $request, Connection $db): Response
{
$this->ensureSchema($db);
$payload = $this->payload($request);
$login = trim((string) ($payload['login'] ?? ''));
$password = (string) ($payload['password'] ?? '');
if ($login === '' || $password === '') {
return $this->error('Введите логин и пароль', 422);
}
$user = $db->fetchAssociative(
'SELECT * FROM users WHERE login = ? OR email = ? LIMIT 1',
[$login, $login]
);
if (!$user || !password_verify($password, (string) $user['password_hash'])) {
return $this->error('Неверный логин или пароль', 401);
}
$plainToken = bin2hex(random_bytes(32));
$db->update('users', ['auth_token_hash' => hash('sha256', $plainToken)], ['id' => $user['id']]);
$request->getSession()->migrate(true);
$request->getSession()->set('hash', $user['hash']);
return $this->tokenResponse($plainToken, [
'ok' => true,
'user' => $this->publicUser($user),
], $request);
}
/**
* @Route("/auth/logout", name="auth_logout", methods={"POST"})
*/
public function logout(Request $request, Connection $db): Response
{
$user = $this->getAuthorizedUser($request, $db);
if ($user) {
$db->update('users', ['auth_token_hash' => null], ['id' => $user['id']]);
}
$request->getSession()->invalidate();
$response = new JsonResponse(['ok' => true]);
$response->headers->clearCookie('auth_token', '/');
return $response;
}
/**
* @Route("/auth/me", name="auth_me", methods={"GET"})
*/
public function me(Request $request, Connection $db): Response
{
$this->ensureSchema($db);
return new JsonResponse([
'user' => $this->publicUser($this->getAuthorizedUser($request, $db)),
], 200, [], false);
}
private function payload(Request $request): array
{
$decoded = json_decode((string) $request->getContent(), true);
if (is_array($decoded)) {
return $decoded;
}
return $request->request->all();
}
private function error(string $message, int $status): JsonResponse
{
return new JsonResponse(['error' => $message], $status, [], false);
}
private function tokenResponse(string $plainToken, array $payload, Request $request): JsonResponse
{
$response = new JsonResponse($payload, 200, [], false);
$response->headers->setCookie(Cookie::create(
'auth_token',
$plainToken,
new \DateTimeImmutable('+7 days'),
'/',
null,
$request->isSecure(),
true,
false,
Cookie::SAMESITE_STRICT
));
return $response;
}
private function ensureSchema(Connection $db): void
{
$platform = $db->getDatabasePlatform()->getName();
if (strpos($platform, 'mysql') !== false) {
$db->executeStatement(
"CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT NOT NULL,
login VARCHAR(50) NOT NULL,
email VARCHAR(180) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
balance DECIMAL(10, 2) NOT NULL DEFAULT 0,
hash VARCHAR(50) NOT NULL,
img VARCHAR(512) NOT NULL DEFAULT '/assets/profile.svg',
auth_token VARCHAR(225) DEFAULT NULL,
auth_token_hash VARCHAR(64) DEFAULT NULL,
ip VARCHAR(50) DEFAULT '',
ref_id INT NOT NULL DEFAULT 0,
refs INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX uniq_users_login (login),
UNIQUE INDEX uniq_users_email (email),
UNIQUE INDEX uniq_users_hash (hash),
UNIQUE INDEX uniq_users_auth_token_hash (auth_token_hash),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB"
);
return;
}
$db->executeStatement(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(180) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
balance NUMERIC NOT NULL DEFAULT 0,
hash VARCHAR(50) NOT NULL UNIQUE,
img VARCHAR(512) NOT NULL DEFAULT '/assets/profile.svg',
auth_token VARCHAR(225) DEFAULT NULL,
auth_token_hash VARCHAR(64) DEFAULT NULL UNIQUE,
ip VARCHAR(50) DEFAULT '',
ref_id INTEGER NOT NULL DEFAULT 0,
refs INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)"
);
}
}