This commit is contained in:
2026-04-12 01:35:19 +02:00
parent 91a8b85f38
commit d04e02020c
70 changed files with 8634 additions and 207 deletions

View File

@@ -29,6 +29,7 @@ final class AuthController
'title' => $this->translator->get('auth.login.title'),
'errorMessage' => Flash::get('error'),
'oldEmail' => (string) Flash::get('old_email', ''),
'oldRemember' => (bool) Flash::get('old_remember', false),
'csrfToken' => Csrf::token(),
], 'layouts/auth');
@@ -38,9 +39,12 @@ final class AuthController
public function login(Request $request): Response
{
$csrfToken = (string) $request->input('_token', '');
$remember = (bool) $request->input('remember', false);
if (!Csrf::validate($csrfToken)) {
Flash::set('error', $this->translator->get('auth.errors.csrf_expired'));
Flash::set('old_email', (string) $request->input('email', ''));
Flash::set('old_remember', $remember);
return Response::redirect('/login');
}
@@ -50,15 +54,24 @@ final class AuthController
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $password === '') {
Flash::set('error', $this->translator->get('auth.errors.invalid_credentials_format'));
Flash::set('old_email', $email);
Flash::set('old_remember', $remember);
return Response::redirect('/login');
}
if (!$this->auth->attempt($email, $password)) {
Flash::set('error', $this->translator->get('auth.errors.invalid_credentials'));
Flash::set('old_email', $email);
Flash::set('old_remember', $remember);
return Response::redirect('/login');
}
if ($remember) {
$user = $this->auth->user();
if ($user !== null) {
$this->auth->createRememberToken((int) $user['id']);
}
}
return Response::redirect('/settings/users');
}

View File

@@ -14,7 +14,7 @@ final class AuthMiddleware
public function __invoke(Request $request, callable $next): Response
{
if (!$this->auth->check()) {
if (!$this->auth->check() && !$this->auth->loginFromRememberToken()) {
return Response::redirect('/login');
}

View File

@@ -9,6 +9,8 @@ use App\Modules\Users\UserRepository;
final class AuthService
{
private const SESSION_USER_KEY = 'auth_user';
private const REMEMBER_COOKIE = 'remember_token';
private const REMEMBER_DAYS = 30;
public function __construct(private readonly UserRepository $users)
{
@@ -57,9 +59,69 @@ final class AuthService
return $user;
}
public function createRememberToken(int $userId): void
{
$token = bin2hex(random_bytes(32));
$this->users->updateRememberToken($userId, hash('sha256', $token));
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
setcookie(self::REMEMBER_COOKIE, $token, [
'expires' => time() + (self::REMEMBER_DAYS * 86400),
'path' => '/',
'httponly' => true,
'secure' => $secure,
'samesite' => 'Lax',
]);
}
public function loginFromRememberToken(): bool
{
$cookieToken = $_COOKIE[self::REMEMBER_COOKIE] ?? '';
if ($cookieToken === '') {
return false;
}
$tokenHash = hash('sha256', $cookieToken);
$user = $this->users->findByRememberToken($tokenHash);
if ($user === null) {
$this->clearRememberCookie();
return false;
}
Session::regenerate();
$_SESSION[self::SESSION_USER_KEY] = [
'id' => (int) $user['id'],
'name' => (string) $user['name'],
'email' => (string) $user['email'],
'login_at' => date(DATE_ATOM),
];
return true;
}
public function logout(): void
{
$user = $this->user();
if ($user !== null) {
$this->users->updateRememberToken((int) $user['id'], null);
}
$this->clearRememberCookie();
unset($_SESSION[self::SESSION_USER_KEY]);
Session::regenerate();
}
private function clearRememberCookie(): void
{
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
setcookie(self::REMEMBER_COOKIE, '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
'secure' => $secure,
'samesite' => 'Lax',
]);
unset($_COOKIE[self::REMEMBER_COOKIE]);
}
}