Atualiza estrutura MVC e integração com banco de dados
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
cat > /home/opc/html/app/Controllers/Controller.php <<'EOF'
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller base da aplicação.
|
||||
*
|
||||
* Esta classe servirá como ponto de partida para
|
||||
* os Controllers específicos do sistema.
|
||||
*/
|
||||
abstract class Controller
|
||||
{
|
||||
}
|
||||
EOF
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class DashboardController
|
||||
{
|
||||
/**
|
||||
* Exibe a página principal do dashboard.
|
||||
*
|
||||
* A autenticação e o controle de expiração
|
||||
* da sessão são realizados pelo security.php.
|
||||
*/
|
||||
public function index(): void
|
||||
{
|
||||
/*
|
||||
* Verifica se existe um usuário autenticado.
|
||||
*
|
||||
* Caso contrário, redireciona para o login.
|
||||
*/
|
||||
if (!isset($_SESSION['usuario_id'])) {
|
||||
header('Location: /login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* Dados básicos do usuário autenticado.
|
||||
*
|
||||
* Posteriormente esses dados poderão vir
|
||||
* de um Model ou de um Service.
|
||||
*/
|
||||
$usuario = [
|
||||
'id' => $_SESSION['usuario_id'],
|
||||
'nome' => $_SESSION['usuario_nome'] ?? '',
|
||||
'email' => $_SESSION['usuario_email'] ?? '',
|
||||
'perfil' => $_SESSION['usuario_perfil'] ?? ''
|
||||
];
|
||||
|
||||
/*
|
||||
* Carrega a View do dashboard.
|
||||
*
|
||||
* A View recebe os dados através da variável
|
||||
* $usuario e fica responsável somente pela
|
||||
* apresentação das informações.
|
||||
*/
|
||||
require __DIR__ . '/../Views/dashboard/index.php';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller responsável pelos mecanismos de segurança
|
||||
* relacionados à sessão e às requisições HTTP.
|
||||
*
|
||||
* Responsabilidades:
|
||||
*
|
||||
* - configurar os parâmetros de segurança da sessão;
|
||||
* - iniciar a sessão com segurança;
|
||||
* - aplicar headers HTTP de segurança;
|
||||
* - controlar o tempo de inatividade da sessão;
|
||||
* - controlar o tempo máximo de uma sessão;
|
||||
* - destruir sessões expiradas;
|
||||
* - redirecionar o usuário quando a sessão expirar.
|
||||
*/
|
||||
class SecurityController
|
||||
{
|
||||
/**
|
||||
* Tempo máximo permitido sem atividade do usuário.
|
||||
*
|
||||
* 1800 segundos = 30 minutos.
|
||||
*/
|
||||
private const SESSAO_INATIVIDADE_SEGUNDOS = 1800;
|
||||
|
||||
/**
|
||||
* Tempo máximo absoluto de uma sessão.
|
||||
*
|
||||
* 28800 segundos = 8 horas.
|
||||
*/
|
||||
private const SESSAO_MAXIMA_SEGUNDOS = 28800;
|
||||
|
||||
/**
|
||||
* Configura os parâmetros de segurança
|
||||
* utilizados pelo cookie da sessão.
|
||||
*
|
||||
* Essas configurações devem ser aplicadas
|
||||
* antes de session_start().
|
||||
*/
|
||||
public function configurarSessao(): void
|
||||
{
|
||||
ini_set(
|
||||
'session.use_strict_mode',
|
||||
'1'
|
||||
);
|
||||
|
||||
ini_set(
|
||||
'session.use_only_cookies',
|
||||
'1'
|
||||
);
|
||||
|
||||
ini_set(
|
||||
'session.cookie_httponly',
|
||||
'1'
|
||||
);
|
||||
|
||||
ini_set(
|
||||
'session.cookie_secure',
|
||||
'1'
|
||||
);
|
||||
|
||||
ini_set(
|
||||
'session.cookie_samesite',
|
||||
'Lax'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicia a sessão caso ainda não exista
|
||||
* uma sessão ativa.
|
||||
*/
|
||||
public function iniciarSessao(): void
|
||||
{
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica headers HTTP destinados a reduzir
|
||||
* diferentes tipos de ataques no navegador.
|
||||
*
|
||||
* Os headers utilizados são:
|
||||
*
|
||||
* - X-Content-Type-Options;
|
||||
* - X-Frame-Options;
|
||||
* - Referrer-Policy;
|
||||
* - Permissions-Policy;
|
||||
* - Content-Security-Policy;
|
||||
* - Strict-Transport-Security.
|
||||
*/
|
||||
public function aplicarHeadersSeguranca(): void
|
||||
{
|
||||
header(
|
||||
'X-Content-Type-Options: nosniff'
|
||||
);
|
||||
|
||||
header(
|
||||
'X-Frame-Options: DENY'
|
||||
);
|
||||
|
||||
header(
|
||||
'Referrer-Policy: strict-origin-when-cross-origin'
|
||||
);
|
||||
|
||||
header(
|
||||
'Permissions-Policy: camera=(), microphone=(), geolocation=()'
|
||||
);
|
||||
|
||||
header(
|
||||
"Content-Security-Policy: "
|
||||
. "default-src 'self'; "
|
||||
. "style-src 'self'; "
|
||||
. "script-src 'self'; "
|
||||
. "img-src 'self' data:; "
|
||||
. "font-src 'self'; "
|
||||
. "form-action 'self'; "
|
||||
. "frame-ancestors 'none'; "
|
||||
. "base-uri 'self'; "
|
||||
. "object-src 'none'"
|
||||
);
|
||||
|
||||
/*
|
||||
* HSTS determina que o navegador deverá utilizar
|
||||
* HTTPS durante o período definido.
|
||||
*
|
||||
* Esta aplicação já utiliza HTTPS através do Traefik.
|
||||
*/
|
||||
header(
|
||||
'Strict-Transport-Security: max-age=31536000; includeSubDomains'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se a sessão autenticada ainda está dentro
|
||||
* dos limites de tempo estabelecidos.
|
||||
*
|
||||
* São verificados dois limites:
|
||||
*
|
||||
* 1. tempo máximo de inatividade;
|
||||
* 2. tempo máximo absoluto da sessão.
|
||||
*/
|
||||
public function verificarExpiracaoSessao(): void
|
||||
{
|
||||
/*
|
||||
* Se não existir usuário autenticado,
|
||||
* não há sessão protegida para verificar.
|
||||
*/
|
||||
if (!isset($_SESSION['usuario_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$agora = time();
|
||||
|
||||
$ultimoAcesso = (int) (
|
||||
$_SESSION['last_activity'] ?? 0
|
||||
);
|
||||
|
||||
$inicioSessao = (int) (
|
||||
$_SESSION['login_time'] ?? 0
|
||||
);
|
||||
|
||||
/*
|
||||
* Verifica se o usuário ficou tempo demais
|
||||
* sem realizar nenhuma atividade.
|
||||
*/
|
||||
if (
|
||||
$ultimoAcesso > 0
|
||||
&& ($agora - $ultimoAcesso)
|
||||
> self::SESSAO_INATIVIDADE_SEGUNDOS
|
||||
) {
|
||||
$this->encerrarSessao(
|
||||
'inatividade'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifica se a sessão atingiu seu tempo
|
||||
* máximo absoluto.
|
||||
*/
|
||||
if (
|
||||
$inicioSessao > 0
|
||||
&& ($agora - $inicioSessao)
|
||||
> self::SESSAO_MAXIMA_SEGUNDOS
|
||||
) {
|
||||
$this->encerrarSessao(
|
||||
'tempo_maximo'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Registra a última atividade do usuário.
|
||||
*/
|
||||
$_SESSION['last_activity'] = $agora;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encerra uma sessão expirada e redireciona
|
||||
* o usuário para a tela de login.
|
||||
*
|
||||
* O motivo da expiração é enviado através
|
||||
* de um parâmetro na URL.
|
||||
*/
|
||||
private function encerrarSessao(
|
||||
string $motivo
|
||||
): void {
|
||||
/*
|
||||
* Remove todos os dados armazenados
|
||||
* na sessão atual.
|
||||
*/
|
||||
$_SESSION = [];
|
||||
|
||||
/*
|
||||
* Remove o cookie da 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();
|
||||
|
||||
/*
|
||||
* Redireciona para o login informando
|
||||
* que a sessão foi encerrada.
|
||||
*/
|
||||
header(
|
||||
'Location: /login.php?expirada=1&motivo='
|
||||
. urlencode($motivo)
|
||||
);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa a sequência básica de segurança
|
||||
* necessária para uma requisição da aplicação.
|
||||
*
|
||||
* Este método permite que as páginas utilizem
|
||||
* apenas uma chamada para inicializar a segurança.
|
||||
*/
|
||||
public function iniciarSeguranca(): void
|
||||
{
|
||||
/*
|
||||
* Configura o cookie antes de iniciar a sessão.
|
||||
*/
|
||||
$this->configurarSessao();
|
||||
|
||||
/*
|
||||
* Inicia a sessão.
|
||||
*/
|
||||
$this->iniciarSessao();
|
||||
|
||||
/*
|
||||
* Aplica os headers HTTP de segurança.
|
||||
*/
|
||||
$this->aplicarHeadersSeguranca();
|
||||
|
||||
/*
|
||||
* Verifica se uma sessão autenticada
|
||||
* expirou por tempo.
|
||||
*/
|
||||
$this->verificarExpiracaoSessao();
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Model responsável pelas operações relacionadas
|
||||
* às tentativas de autenticação.
|
||||
*
|
||||
* Responsabilidades:
|
||||
*
|
||||
* - verificar tentativas inválidas por e-mail;
|
||||
* - verificar tentativas inválidas por endereço IP;
|
||||
* - registrar tentativas de autenticação;
|
||||
* - centralizar o acesso à tabela tentativas_login.
|
||||
*/
|
||||
class LoginAttempt
|
||||
{
|
||||
/**
|
||||
* Conexão com o banco de dados.
|
||||
*/
|
||||
private PDO $pdo;
|
||||
|
||||
|
||||
/**
|
||||
* Recebe a conexão PDO utilizada pelo Model.
|
||||
*/
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Conta as tentativas inválidas de um endereço
|
||||
* de e-mail dentro da janela de tempo informada.
|
||||
*/
|
||||
public function contarFalhasPorEmail(
|
||||
string $email,
|
||||
int $janelaMinutos
|
||||
): int {
|
||||
|
||||
$janelaMinutos = max(
|
||||
1,
|
||||
$janelaMinutos
|
||||
);
|
||||
|
||||
$sql = "
|
||||
SELECT COUNT(*)
|
||||
FROM tentativas_login
|
||||
WHERE email = :email
|
||||
AND sucesso = 0
|
||||
AND criado_em >= DATE_SUB(
|
||||
NOW(),
|
||||
INTERVAL {$janelaMinutos} MINUTE
|
||||
)
|
||||
";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
$stmt->execute([
|
||||
'email' => $email
|
||||
]);
|
||||
|
||||
return (int)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Conta as tentativas inválidas de um endereço
|
||||
* IP dentro da janela de tempo informada.
|
||||
*/
|
||||
public function contarFalhasPorIp(
|
||||
string $ip,
|
||||
int $janelaMinutos
|
||||
): int {
|
||||
|
||||
$janelaMinutos = max(
|
||||
1,
|
||||
$janelaMinutos
|
||||
);
|
||||
|
||||
$sql = "
|
||||
SELECT COUNT(*)
|
||||
FROM tentativas_login
|
||||
WHERE ip_address = :ip
|
||||
AND sucesso = 0
|
||||
AND criado_em >= DATE_SUB(
|
||||
NOW(),
|
||||
INTERVAL {$janelaMinutos} MINUTE
|
||||
)
|
||||
";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
$stmt->execute([
|
||||
'ip' => $ip
|
||||
]);
|
||||
|
||||
return (int)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registra uma tentativa de autenticação
|
||||
* realizada pelo usuário.
|
||||
*/
|
||||
public function registrar(
|
||||
string $email,
|
||||
string $ip,
|
||||
bool $sucesso
|
||||
): bool {
|
||||
|
||||
$sql = "
|
||||
INSERT INTO tentativas_login
|
||||
(
|
||||
email,
|
||||
ip_address,
|
||||
sucesso
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
:email,
|
||||
:ip,
|
||||
:sucesso
|
||||
)
|
||||
";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
return $stmt->execute([
|
||||
'email' => $email,
|
||||
'ip' => $ip,
|
||||
'sucesso' => $sucesso ? 1 : 0
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Model responsável pelas operações relacionadas
|
||||
* aos usuários da aplicação.
|
||||
*
|
||||
* Responsabilidades:
|
||||
*
|
||||
* - localizar usuário pelo e-mail;
|
||||
* - atualizar senha;
|
||||
* - centralizar o acesso à tabela usuarios.
|
||||
*/
|
||||
class User
|
||||
{
|
||||
/**
|
||||
* Conexão com o banco de dados.
|
||||
*/
|
||||
private PDO $pdo;
|
||||
|
||||
|
||||
/**
|
||||
* Recebe a conexão PDO utilizada pelo Model.
|
||||
*/
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Localiza um usuário através do endereço de e-mail.
|
||||
*
|
||||
* Retorna os dados necessários para o processo
|
||||
* de autenticação.
|
||||
*
|
||||
* Retorna null quando o usuário não existe.
|
||||
*/
|
||||
public function buscarPorEmail(string $email): ?array
|
||||
{
|
||||
$sql = "
|
||||
SELECT
|
||||
id,
|
||||
nome,
|
||||
email,
|
||||
senha,
|
||||
perfil,
|
||||
ativo
|
||||
FROM usuarios
|
||||
WHERE email = :email
|
||||
LIMIT 1
|
||||
";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
$stmt->execute([
|
||||
'email' => $email
|
||||
]);
|
||||
|
||||
$usuario = $stmt->fetch();
|
||||
|
||||
if ($usuario === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $usuario;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Atualiza o hash da senha de um usuário.
|
||||
*
|
||||
* Utilizado quando password_needs_rehash()
|
||||
* identificar que o algoritmo ou os parâmetros
|
||||
* utilizados para criar o hash foram atualizados.
|
||||
*/
|
||||
public function atualizarSenha(
|
||||
int $id,
|
||||
string $novoHash
|
||||
): bool {
|
||||
|
||||
$sql = "
|
||||
UPDATE usuarios
|
||||
SET senha = :senha
|
||||
WHERE id = :id
|
||||
";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
return $stmt->execute([
|
||||
'senha' => $novoHash,
|
||||
'id' => $id
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="pt-BR">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="Acesso ao sistema Finanças+"
|
||||
>
|
||||
|
||||
<title>Acesso - Finanças+</title>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/css/style.css"
|
||||
>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
|
||||
<nav class="nav container">
|
||||
|
||||
<a
|
||||
class="brand"
|
||||
href="/"
|
||||
>
|
||||
|
||||
<span class="mark">
|
||||
F+
|
||||
</span>
|
||||
|
||||
Finanças<span>+</span>
|
||||
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<section class="section">
|
||||
|
||||
<div
|
||||
class="container"
|
||||
style="max-width: 480px;"
|
||||
>
|
||||
|
||||
<div class="heading">
|
||||
|
||||
<small class="eyebrow">
|
||||
ACESSO AO SISTEMA
|
||||
</small>
|
||||
|
||||
<!--<h1>
|
||||
Entrar
|
||||
</h1>-->
|
||||
|
||||
<p>
|
||||
Acesse sua área financeira.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($erro !== ''): ?>
|
||||
|
||||
<div
|
||||
role="alert" class="login-error"
|
||||
>
|
||||
|
||||
<?= htmlspecialchars(
|
||||
$erro,
|
||||
ENT_QUOTES | ENT_SUBSTITUTE,
|
||||
'UTF-8'
|
||||
) ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="login-card">
|
||||
<form
|
||||
method="post"
|
||||
action="/login.php"
|
||||
autocomplete="on"
|
||||
>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="<?= htmlspecialchars(
|
||||
$_SESSION['csrf_token'],
|
||||
ENT_QUOTES | ENT_SUBSTITUTE,
|
||||
'UTF-8'
|
||||
) ?>"
|
||||
>
|
||||
|
||||
<label>
|
||||
E-mail
|
||||
|
||||
<div class="input">
|
||||
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
maxlength="150"
|
||||
required
|
||||
autocomplete="username"
|
||||
value="<?= htmlspecialchars(
|
||||
$email,
|
||||
ENT_QUOTES | ENT_SUBSTITUTE,
|
||||
'UTF-8'
|
||||
) ?>"
|
||||
>
|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
<br>
|
||||
|
||||
<label>
|
||||
Senha
|
||||
|
||||
<div class="input">
|
||||
|
||||
<input
|
||||
type="password"
|
||||
name="senha"
|
||||
maxlength="128"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
>
|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn primary full"
|
||||
>
|
||||
Entrar
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
|
||||
<div class="container footer">
|
||||
|
||||
<div>
|
||||
|
||||
<a
|
||||
class="brand"
|
||||
href="/"
|
||||
>
|
||||
|
||||
<span class="mark">
|
||||
F+
|
||||
</span>
|
||||
|
||||
Finanças<span>+</span>
|
||||
|
||||
</a>
|
||||
|
||||
<p>
|
||||
Conhecimento para decisões financeiras melhores.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<small>
|
||||
© 2026 Finanças+.
|
||||
</small>
|
||||
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="pt-BR">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="Dashboard financeiro do Finanças+"
|
||||
>
|
||||
|
||||
<title>Dashboard - Finanças+</title>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/css/style.css"
|
||||
>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
|
||||
<nav class="nav container">
|
||||
|
||||
<a
|
||||
class="brand"
|
||||
href="/"
|
||||
>
|
||||
|
||||
<span class="mark">
|
||||
F+
|
||||
</span>
|
||||
|
||||
Finanças<span>+</span>
|
||||
|
||||
</a>
|
||||
|
||||
<div class="links">
|
||||
|
||||
<a href="/dashboard.php">
|
||||
Dashboard
|
||||
</a>
|
||||
|
||||
<a href="/logout.php">
|
||||
Sair
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<section class="section">
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="heading">
|
||||
|
||||
<small class="eyebrow">
|
||||
ÁREA DO USUÁRIO
|
||||
</small>
|
||||
|
||||
<h1>
|
||||
Olá, <?= htmlspecialchars(
|
||||
$usuario['nome'],
|
||||
ENT_QUOTES | ENT_SUBSTITUTE,
|
||||
'UTF-8'
|
||||
) ?>!
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
Bem-vindo ao seu painel financeiro.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<article class="card">
|
||||
|
||||
<div class="icon">
|
||||
??
|
||||
</div>
|
||||
|
||||
<small>
|
||||
PATRIMÔNIO
|
||||
</small>
|
||||
|
||||
<h3>
|
||||
R$ 0,00
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Seu patrimônio será exibido aqui.
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
|
||||
<div class="icon">
|
||||
??
|
||||
</div>
|
||||
|
||||
<small>
|
||||
RECEITAS
|
||||
</small>
|
||||
|
||||
<h3>
|
||||
R$ 0,00
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Acompanhe suas receitas mensais.
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
|
||||
<div class="icon">
|
||||
??
|
||||
</div>
|
||||
|
||||
<small>
|
||||
INVESTIMENTOS
|
||||
</small>
|
||||
|
||||
<h3>
|
||||
R$ 0,00
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Acompanhe a evolução dos seus investimentos.
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
|
||||
<div class="container footer">
|
||||
|
||||
<div>
|
||||
|
||||
<a
|
||||
class="brand"
|
||||
href="/"
|
||||
>
|
||||
|
||||
<span class="mark">
|
||||
F+
|
||||
</span>
|
||||
|
||||
Finanças<span>+</span>
|
||||
|
||||
</a>
|
||||
|
||||
<p>
|
||||
Conhecimento para decisões financeiras melhores.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<small>
|
||||
© 2026 Finanças+.
|
||||
</small>
|
||||
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user