Atualiza estrutura MVC e integração com banco de dados

This commit is contained in:
2026-08-30 22:18:31 -03:00
parent 6231cc46a8
commit 966f6b7587
26 changed files with 3265 additions and 8 deletions
+515
View File
@@ -0,0 +1,515 @@
<?php
declare(strict_types=1);
class AuthController
{
/**
* Model responsável pelos usuários.
*/
private User $user;
/**
* Model responsável pelas tentativas
* de autenticação.
*/
private LoginAttempt $loginAttempt;
/**
* Número máximo de tentativas inválidas
* permitidas para uma mesma conta.
*/
private const MAX_TENTATIVAS_CONTA = 5;
/**
* Número máximo de tentativas inválidas
* permitidas para um mesmo endereço IP.
*/
private const MAX_TENTATIVAS_IP = 20;
/**
* Período utilizado para contabilizar
* as tentativas inválidas.
*/
private const JANELA_MINUTOS = 10;
/**
* Recebe os Models necessários para
* executar o processo de autenticação.
*/
public function __construct(
User $user,
LoginAttempt $loginAttempt
) {
$this->user = $user;
$this->loginAttempt = $loginAttempt;
}
/**
* Processa o acesso à página de login.
*
* GET:
* Exibe o formulário.
*
* POST:
* Processa a tentativa de autenticação.
*/
public function login(): void
{
if ($this->usuarioAutenticado()) {
header('Location: /dashboard.php');
exit;
}
/*
* Garante que a sessão possua
* um token CSRF.
*/
$this->gerarTokenCsrf();
$erro = '';
$email = trim(
(string)($_POST['email'] ?? '')
);
$senha = (string)(
$_POST['senha'] ?? ''
);
/*
* Obtém o endereço IP real do cliente.
*/
$ip = $this->obterIpCliente();
/*
* Processa somente requisições POST.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$erro = $this->processarLogin(
$email,
$senha,
$ip
);
}
/*
* Carrega a View responsável
* pela apresentação do formulário.
*/
require __DIR__ . '/../Views/auth/login.php';
}
/**
* Verifica se existe um usuário autenticado
* na sessão atual.
*/
private function usuarioAutenticado(): bool
{
return isset(
$_SESSION['usuario_id']
);
}
/**
* Cria o token CSRF caso ainda
* não exista na sessão.
*/
private function gerarTokenCsrf(): void
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(
random_bytes(32)
);
}
}
/**
* Processa todas as etapas de uma
* tentativa de autenticação.
*/
private function processarLogin(
string $email,
string $senha,
string $ip
): string {
/*
* Valida o token CSRF enviado
* pelo formulário.
*/
if (!$this->validarCsrf()) {
return 'E-mail ou senha inválidos.';
}
/*
* Valida os dados básicos enviados
* pelo usuário.
*/
if (
!$this->validarDadosLogin(
$email,
$senha
)
) {
return 'E-mail ou senha inválidos.';
}
/*
* Verifica o limite de tentativas
* para a conta.
*/
if ($this->contaBloqueada($email)) {
return 'E-mail ou senha inválidos.';
}
/*
* Verifica o limite de tentativas
* para o endereço IP.
*/
if ($this->ipBloqueado($ip)) {
return 'E-mail ou senha inválidos.';
}
/*
* Solicita ao Model o usuário
* correspondente ao e-mail informado.
*/
$usuario = $this->user->buscarPorEmail(
$email
);
/*
* Verifica as credenciais fornecidas.
*/
if (
$usuario !== null &&
(int)$usuario['ativo'] === 1 &&
password_verify(
$senha,
$usuario['senha']
)
) {
/*
* Registra a tentativa bem-sucedida.
*/
$this->loginAttempt->registrar(
$email,
$ip,
true
);
/*
* Cria a sessão autenticada.
*/
$this->criarSessaoAutenticada(
$usuario
);
/*
* Atualiza o hash da senha caso
* seja necessário utilizar parâmetros
* mais atuais.
*/
$this->atualizarHashSeNecessario(
$usuario,
$senha
);
header('Location: /dashboard.php');
exit;
}
/*
* Registra a tentativa inválida.
*/
$this->loginAttempt->registrar(
$email,
$ip,
false
);
/*
* Pequeno atraso para dificultar
* ataques automatizados.
*/
usleep(300000);
return 'E-mail ou senha inválidos.';
}
/**
* Valida o token CSRF enviado
* pelo formulário de login.
*/
private function validarCsrf(): bool
{
$token = (string)(
$_POST['csrf_token'] ?? ''
);
return (
$token !== '' &&
isset($_SESSION['csrf_token']) &&
hash_equals(
$_SESSION['csrf_token'],
$token
)
);
}
/**
* Valida os dados básicos utilizados
* durante a autenticação.
*/
private function validarDadosLogin(
string $email,
string $senha
): bool {
if (strlen($email) > 150) {
return false;
}
if (
$email === '' ||
$senha === ''
) {
return false;
}
if (
filter_var(
$email,
FILTER_VALIDATE_EMAIL
) === false
) {
return false;
}
return true;
}
/**
* Obtém o endereço IP real do cliente.
*
* Quando a requisição passa pelo Traefik,
* REMOTE_ADDR corresponde ao IP do Traefik.
*
* Nesse caso utilizamos X-Real-IP,
* conforme configurado no ambiente.
*/
private function obterIpCliente(): string
{
$remoteAddr =
$_SERVER['REMOTE_ADDR'] ?? '';
if ($remoteAddr === '172.18.0.3') {
$ip =
$_SERVER['HTTP_X_REAL_IP'] ?? '';
if (
filter_var(
$ip,
FILTER_VALIDATE_IP
) !== false
) {
return $ip;
}
}
if (
filter_var(
$remoteAddr,
FILTER_VALIDATE_IP
) !== false
) {
return $remoteAddr;
}
return '0.0.0.0';
}
/**
* Verifica se o endereço de e-mail
* atingiu o limite de tentativas inválidas.
*/
private function contaBloqueada(
string $email
): bool {
$quantidade =
$this->loginAttempt
->contarFalhasPorEmail(
$email,
self::JANELA_MINUTOS
);
return $quantidade
>= self::MAX_TENTATIVAS_CONTA;
}
/**
* Verifica se o endereço IP atingiu
* o limite de tentativas inválidas.
*/
private function ipBloqueado(
string $ip
): bool {
$quantidade =
$this->loginAttempt
->contarFalhasPorIp(
$ip,
self::JANELA_MINUTOS
);
return $quantidade
>= self::MAX_TENTATIVAS_IP;
}
/**
* Cria a sessão do usuário após
* uma autenticação bem-sucedida.
*
* A regeneração do ID impede ataques
* de session fixation.
*/
private function criarSessaoAutenticada(
array $usuario
): void {
session_regenerate_id(true);
/*
* Gera um novo token CSRF depois
* da autenticação.
*/
$_SESSION['csrf_token'] = bin2hex(
random_bytes(32)
);
$_SESSION['usuario_id'] =
$usuario['id'];
$_SESSION['usuario_nome'] =
$usuario['nome'];
$_SESSION['usuario_email'] =
$usuario['email'];
$_SESSION['usuario_perfil'] =
$usuario['perfil'];
/*
* Registra os tempos utilizados
* pelo controle de expiração da sessão.
*/
$_SESSION['login_time'] = time();
$_SESSION['last_activity'] = time();
}
/**
* Atualiza o hash da senha caso o algoritmo
* ou seus parâmetros tenham evoluído.
*
* A operação de banco é realizada pelo
* Model User.
*/
private function atualizarHashSeNecessario(
array $usuario,
string $senha
): void {
if (
!password_needs_rehash(
$usuario['senha'],
PASSWORD_DEFAULT
)
) {
return;
}
$novoHash = password_hash(
$senha,
PASSWORD_DEFAULT
);
$this->user->atualizarSenha(
(int)$usuario['id'],
$novoHash
);
}
/**
* Encerra a sessão do usuário.
*
* Remove os dados da sessão, invalida
* o cookie de sessão e destrói a sessão
* existente no servidor.
*/
public function logout(): void
{
/*
* Remove todos os dados armazenados
* na sessão atual.
*/
$_SESSION = [];
/*
* Remove o cookie de sessão
* do navegador.
*/
if (ini_get('session.use_cookies')) {
$params =
session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
(bool)$params['secure'],
(bool)$params['httponly']
);
}
/*
* Destrói a sessão no servidor.
*/
session_destroy();
/*
* Retorna o usuário para
* a tela de login.
*/
header('Location: /login.php');
exit;
}
}