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>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once '/home/opc/database.php';
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Configurações centralizadas de segurança da aplicação.
|
||||
*
|
||||
* Responsabilidades:
|
||||
*
|
||||
* - configurar parâmetros seguros da sessão;
|
||||
* - configurar headers HTTP de segurança;
|
||||
* - iniciar a sessão;
|
||||
* - controlar expiração por inatividade;
|
||||
* - controlar tempo máximo da sessão.
|
||||
*
|
||||
* Este arquivo deve ser carregado antes de qualquer
|
||||
* saída HTML ou envio de headers HTTP.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* CONFIGURAÇÕES DA SESSÃO
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tempo máximo de inatividade permitido.
|
||||
*
|
||||
* 1800 segundos = 30 minutos.
|
||||
*/
|
||||
const SESSAO_INATIVIDADE_SEGUNDOS = 1800;
|
||||
|
||||
|
||||
/**
|
||||
* Tempo máximo absoluto de uma sessão autenticada.
|
||||
*
|
||||
* 28800 segundos = 8 horas.
|
||||
*/
|
||||
const SESSAO_MAXIMA_SEGUNDOS = 28800;
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* CONFIGURAÇÃO DOS COOKIES DA SESSÃO
|
||||
* =========================================================
|
||||
*
|
||||
* Estas configurações precisam ser realizadas
|
||||
* antes de session_start().
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Impede a utilização de um identificador de sessão
|
||||
* que não tenha sido criado pelo servidor.
|
||||
*/
|
||||
ini_set(
|
||||
'session.use_strict_mode',
|
||||
'1'
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Permite que o identificador da sessão seja enviado
|
||||
* somente através de cookie.
|
||||
*
|
||||
* Isso impede o uso de IDs de sessão na URL.
|
||||
*/
|
||||
ini_set(
|
||||
'session.use_only_cookies',
|
||||
'1'
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Impede que JavaScript tenha acesso ao cookie
|
||||
* da sessão.
|
||||
*/
|
||||
ini_set(
|
||||
'session.cookie_httponly',
|
||||
'1'
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Permite o envio do cookie somente através
|
||||
* de conexões HTTPS.
|
||||
*/
|
||||
ini_set(
|
||||
'session.cookie_secure',
|
||||
'1'
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Reduz o risco de envio automático do cookie
|
||||
* em requisições originadas de outros sites.
|
||||
*/
|
||||
ini_set(
|
||||
'session.cookie_samesite',
|
||||
'Lax'
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* CONFIGURAÇÕES GERAIS DO PHP
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Não informa ao navegador que a aplicação
|
||||
* está utilizando PHP.
|
||||
*/
|
||||
ini_set(
|
||||
'expose_php',
|
||||
'0'
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* HEADERS HTTP DE SEGURANÇA
|
||||
* =========================================================
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Impede que o navegador interprete um conteúdo
|
||||
* utilizando um MIME type diferente daquele declarado.
|
||||
*/
|
||||
header(
|
||||
'X-Content-Type-Options: nosniff'
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Impede que a aplicação seja carregada
|
||||
* dentro de frames.
|
||||
*
|
||||
* Proteção contra ataques de clickjacking.
|
||||
*/
|
||||
header(
|
||||
'X-Frame-Options: DENY'
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Controla quais informações de referência
|
||||
* podem ser enviadas pelo navegador.
|
||||
*/
|
||||
header(
|
||||
'Referrer-Policy: strict-origin-when-cross-origin'
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Desabilita recursos do navegador que não são
|
||||
* utilizados pela aplicação.
|
||||
*/
|
||||
header(
|
||||
'Permissions-Policy: camera=(), microphone=(), geolocation=()'
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* CONTENT SECURITY POLICY
|
||||
* =========================================================
|
||||
*
|
||||
* Permite somente recursos provenientes da própria
|
||||
* aplicação.
|
||||
*
|
||||
* A aplicação utiliza:
|
||||
*
|
||||
* /css/style.css
|
||||
* /js/app.js
|
||||
*
|
||||
* Não são permitidos scripts ou estilos externos.
|
||||
*/
|
||||
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'"
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* HTTP STRICT TRANSPORT SECURITY
|
||||
* =========================================================
|
||||
*
|
||||
* Informa ao navegador que a aplicação deve ser
|
||||
* acessada exclusivamente através de HTTPS.
|
||||
*
|
||||
* O domínio já está funcionando através de HTTPS,
|
||||
* portanto este header pode ser utilizado.
|
||||
*/
|
||||
header(
|
||||
'Strict-Transport-Security: max-age=31536000; includeSubDomains'
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* INICIALIZAÇÃO DA SESSÃO
|
||||
* =========================================================
|
||||
*
|
||||
* A sessão somente será iniciada se ainda não estiver
|
||||
* ativa.
|
||||
*
|
||||
* Isso evita:
|
||||
*
|
||||
* "session_start(): Ignoring session_start()
|
||||
* because a session is already active"
|
||||
*/
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* CONTROLE DE EXPIRAÇÃO DA SESSÃO
|
||||
* =========================================================
|
||||
*
|
||||
* O controle abaixo somente é executado para usuários
|
||||
* que já estejam autenticados.
|
||||
*/
|
||||
if (isset($_SESSION['usuario_id'])) {
|
||||
|
||||
$agora = time();
|
||||
|
||||
$ultimoAcesso =
|
||||
(int)($_SESSION['last_activity'] ?? 0);
|
||||
|
||||
$inicioSessao =
|
||||
(int)($_SESSION['login_time'] ?? 0);
|
||||
|
||||
|
||||
/*
|
||||
* =====================================================
|
||||
* EXPIRAÇÃO POR INATIVIDADE
|
||||
* =====================================================
|
||||
*
|
||||
* Se o usuário permanecer mais de 30 minutos
|
||||
* sem realizar nenhuma requisição autenticada,
|
||||
* a sessão será encerrada.
|
||||
*/
|
||||
if (
|
||||
$ultimoAcesso > 0 &&
|
||||
($agora - $ultimoAcesso)
|
||||
> SESSAO_INATIVIDADE_SEGUNDOS
|
||||
) {
|
||||
|
||||
destruirSessao();
|
||||
|
||||
header(
|
||||
'Location: /login.php?expirada=1'
|
||||
);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =====================================================
|
||||
* EXPIRAÇÃO PELO TEMPO MÁXIMO
|
||||
* =====================================================
|
||||
*
|
||||
* Mesmo que o usuário continue ativo,
|
||||
* a sessão não poderá ultrapassar 8 horas.
|
||||
*/
|
||||
if (
|
||||
$inicioSessao > 0 &&
|
||||
($agora - $inicioSessao)
|
||||
> SESSAO_MAXIMA_SEGUNDOS
|
||||
) {
|
||||
|
||||
destruirSessao();
|
||||
|
||||
header(
|
||||
'Location: /login.php?expirada=1'
|
||||
);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =====================================================
|
||||
* ATUALIZAÇÃO DA ATIVIDADE
|
||||
* =====================================================
|
||||
*
|
||||
* A cada requisição autenticada, registra o momento
|
||||
* do último acesso.
|
||||
*/
|
||||
$_SESSION['last_activity'] = $agora;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* =========================================================
|
||||
* FUNÇÃO DE DESTRUIÇÃO DA SESSÃO
|
||||
* =========================================================
|
||||
*
|
||||
* Centraliza o procedimento utilizado quando uma sessão
|
||||
* precisa ser encerrada.
|
||||
*/
|
||||
function destruirSessao(): void
|
||||
{
|
||||
/*
|
||||
* Remove todos os dados armazenados na sessão.
|
||||
*/
|
||||
$_SESSION = [];
|
||||
|
||||
|
||||
/*
|
||||
* Remove o cookie da sessão no 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 existente no servidor.
|
||||
*/
|
||||
session_destroy();
|
||||
}
|
||||
+356
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/config/security.php';
|
||||
require_once __DIR__ . '/app/Controllers/DashboardController.php';
|
||||
|
||||
$controller = new DashboardController();
|
||||
|
||||
$controller->index();
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
function usuarioAutenticado(): bool
|
||||
{
|
||||
return isset($_SESSION['usuario_id']);
|
||||
}
|
||||
|
||||
function exigirLogin(): void
|
||||
{
|
||||
if (!usuarioAutenticado()) {
|
||||
header('Location: /login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
+456
-7
@@ -1,7 +1,456 @@
|
||||
<!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="Finanças+ — educação financeira e simulador de juros compostos"><title>Finanças+ | Educação Financeira</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"><link rel="stylesheet" href="css/style.css"></head><body>
|
||||
<header class="header"><nav class="nav container"><a class="brand" href="#inicio"><span class="mark">F+</span>Finanças<span>+</span></a><button class="menu" aria-label="Menu">☰</button><div class="links"><a href="#inicio">Início</a><a href="#aprenda">Aprenda</a><a href="#simulador">Simulador</a><a href="#metas">Metas</a></div></nav></header>
|
||||
<main><section class="hero" id="inicio"><div class="orb one"></div><div class="orb two"></div><div class="container hero-grid"><div class="hero-copy reveal"><small class="eyebrow">EDUCAÇÃO FINANCEIRA</small><h1>Seu dinheiro deve <em>trabalhar por você.</em></h1><p>Aprenda a organizar suas finanças, tomar decisões melhores e construir um futuro financeiro mais tranquilo.</p><div class="actions"><a class="btn primary" href="#simulador">Começar a simular →</a><a class="btn outline" href="#aprenda">Quero aprender</a></div><div class="trust">✓ Conteúdo educativo ✓ Simulação gratuita ✓ Sem cadastro</div></div><div class="visual reveal delay1"><div class="dashboard"><div class="dash-top"><span>Patrimônio projetado</span><b>●</b></div><strong>R$ 84.750,00</strong><div class="mini"><svg viewBox="0 0 500 180" preserveAspectRatio="none"><path class="fill" d="M0 160 C55 150 75 130 120 138 S180 115 220 120 S270 88 310 98 S360 60 395 72 S450 30 500 22 L500 180 L0 180Z"/><path class="line" d="M0 160 C55 150 75 130 120 138 S180 115 220 120 S270 88 310 98 S360 60 395 72 S450 30 500 22"/></svg></div><div class="dash-bottom"><span>+18,4%</span><span>12 meses</span></div></div><div class="float goal">🎯 <div><small>Meta mensal</small><b>R$ 500</b></div></div><div class="float saved">💰 <div><small>Economizado</small><b>R$ 7.280</b></div></div></div></div></section>
|
||||
<section class="section" id="aprenda"><div class="container"><div class="heading reveal"><small class="eyebrow">PRIMEIROS PASSOS</small><h2>Uma vida financeira melhor começa com três hábitos.</h2><p>Conhecimento, organização e constância fazem mais diferença do que procurar uma fórmula mágica.</p></div><div class="cards"><article class="card reveal"><div class="icon">💰</div><small>01</small><h3>Organize</h3><p>Saiba quanto entra, quanto sai e para onde seu dinheiro está indo.</p></article><article class="card reveal delay1"><div class="icon">📊</div><small>02</small><h3>Planeje</h3><p>Transforme objetivos em metas financeiras claras e mensuráveis.</p></article><article class="card reveal delay2"><div class="icon">📈</div><small>03</small><h3>Invista</h3><p>Conheça as opções de investimento e o efeito dos juros compostos.</p></article></div></div></section>
|
||||
<section class="sim-section" id="simulador"><div class="container sim-grid"><div class="sim-copy reveal"><small class="eyebrow">SIMULADOR</small><h2>Veja o poder dos juros compostos.</h2><p>Faça uma projeção educativa do crescimento do seu patrimônio. Não é recomendação de investimento.</p><div class="tip">💡 <div><b>Uma regra importante</b><p>Tempo e aportes consistentes podem fazer grande diferença no longo prazo.</p></div></div></div><div class="sim-card reveal delay1"><form id="form"><div class="inputs"><label>Investimento inicial<div class="input"><span>R$</span><input id="initial" type="number" value="1000" min="0"></div></label><label>Aporte mensal<div class="input"><span>R$</span><input id="monthly" type="number" value="300" min="0"></div></label><label>Taxa mensal<div class="input"><input id="rate" type="number" value="0.8" min="0" step="0.1"><span>%</span></div></label><label>Período<div class="input"><input id="months" type="number" value="60" min="1"><span>meses</span></div></label></div><button class="btn primary full">Calcular projeção →</button></form><div class="results"><div><small>Total investido</small><b id="invested">R$ 0,00</b></div><div><small>Rendimentos</small><b id="earnings">R$ 0,00</b></div><div class="highlight"><small>Valor projetado</small><b id="future">R$ 0,00</b></div></div><canvas id="growth"></canvas></div></div></section>
|
||||
<section class="section" id="metas"><div class="container goal-grid"><div class="heading reveal"><small class="eyebrow">METAS FINANCEIRAS</small><h2>Transforme sonhos em números.</h2><p>Uma meta clara facilita acompanhar o progresso e manter a disciplina.</p></div><div class="goal-card reveal delay1"><div class="goal-head"><b>Reserva de emergência</b><strong id="percent">64%</strong></div><div class="progress"><span id="progress"></span></div><div class="goal-values"><span>R$ 6.400</span><span>R$ 10.000</span></div><div class="goal-foot"><span>Faltam R$ 3.600</span><button id="advance">Registrar progresso</button></div></div></div></section><section class="quote"><div class="container reveal"><span>“</span><p>Educação financeira não é sobre ficar rico rapidamente. É sobre fazer escolhas conscientes com o dinheiro que você tem.</p></div></section></main>
|
||||
<footer><div class="container footer"><div><a class="brand" href="#inicio"><span class="mark">F+</span>Finanças<span>+</span></a><p>Conhecimento para decisões financeiras melhores.</p></div><div class="footer-links"><a href="#aprenda">Aprenda</a><a href="#simulador">Simulador</a><a href="#metas">Metas</a></div><small>© <span id="year"></span> Finanças+.</small></div></footer><script src="js/app.js"></script></body></html>
|
||||
<!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="Finanças+ — educação financeira e simulador de juros compostos">
|
||||
<title>Finanças+ | Educação Financeira</title>
|
||||
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<nav class="nav container">
|
||||
|
||||
|
||||
<a class="brand" href="#inicio">
|
||||
<span class="mark">F+</span>Finanças<span>+</span>
|
||||
</a>
|
||||
|
||||
<button class="menu" aria-label="Menu">☰</button>
|
||||
|
||||
<div class="links">
|
||||
<a href="#inicio">Início</a>
|
||||
<a href="#aprenda">Aprenda</a>
|
||||
<a href="#simulador">Simulador</a>
|
||||
<a href="login.php">Entrar</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<section class="hero" id="inicio">
|
||||
|
||||
<div class="orb one"></div>
|
||||
<div class="orb two"></div>
|
||||
|
||||
<div class="container hero-grid">
|
||||
|
||||
<div class="hero-copy reveal">
|
||||
|
||||
<small class="eyebrow">EDUCAÇÃO FINANCEIRA</small>
|
||||
|
||||
<h1>
|
||||
Seu dinheiro deve <em>trabalhar por você.</em>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
Aprenda a organizar suas finanças, tomar decisões melhores
|
||||
e construir um futuro financeiro mais tranquilo.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a class="btn primary" href="#simulador">
|
||||
Começar a simular →
|
||||
</a>
|
||||
|
||||
<a class="btn outline" href="#aprenda">
|
||||
Quero aprender
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="trust">
|
||||
✓ Conteúdo educativo
|
||||
✓ Simulação gratuita
|
||||
✓ Área do aluno
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="visual reveal delay1">
|
||||
|
||||
<div class="dashboard">
|
||||
|
||||
<div class="dash-top">
|
||||
<span>Patrimônio projetado</span>
|
||||
<b>●</b>
|
||||
</div>
|
||||
|
||||
<strong>R$ 84.750,00</strong>
|
||||
|
||||
<div class="mini">
|
||||
<svg viewBox="0 0 500 180" preserveAspectRatio="none">
|
||||
<path
|
||||
class="fill"
|
||||
d="M0 160 C55 150 75 130 120 138 S180 115 220 120 S270 88 310 98 S360 60 395 72 S450 30 500 22 L500 180 L0 180Z"
|
||||
/>
|
||||
<path
|
||||
class="line"
|
||||
d="M0 160 C55 150 75 130 120 138 S180 115 220 120 S270 88 310 98 S360 60 395 72 S450 30 500 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="dash-bottom">
|
||||
<span>+18,4%</span>
|
||||
<span>12 meses</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="float goal">
|
||||
🎯
|
||||
<div>
|
||||
<small>Meta mensal</small>
|
||||
<b>R$ 500</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="float saved">
|
||||
💰
|
||||
<div>
|
||||
<small>Economizado</small>
|
||||
<b>R$ 7.280</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section class="section" id="aprenda">
|
||||
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="heading reveal">
|
||||
|
||||
<small class="eyebrow">PRIMEIROS PASSOS</small>
|
||||
|
||||
<h2>
|
||||
Uma vida financeira melhor começa com três hábitos.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
Conhecimento, organização e constância fazem mais diferença
|
||||
do que procurar uma fórmula mágica.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<article class="card reveal">
|
||||
|
||||
<div class="icon">💰</div>
|
||||
|
||||
<small>01</small>
|
||||
|
||||
<h3>Organize</h3>
|
||||
|
||||
<p>
|
||||
Saiba quanto entra, quanto sai e para onde seu dinheiro está indo.
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card reveal delay1">
|
||||
|
||||
<div class="icon">📊</div>
|
||||
|
||||
<small>02</small>
|
||||
|
||||
<h3>Planeje</h3>
|
||||
|
||||
<p>
|
||||
Transforme objetivos em metas financeiras claras e mensuráveis.
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card reveal delay2">
|
||||
|
||||
<div class="icon">📈</div>
|
||||
|
||||
<small>03</small>
|
||||
|
||||
<h3>Invista</h3>
|
||||
|
||||
<p>
|
||||
Conheça as opções de investimento e o efeito dos juros compostos.
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section class="sim-section" id="simulador">
|
||||
|
||||
|
||||
<div class="container sim-grid">
|
||||
|
||||
<div class="sim-copy reveal">
|
||||
|
||||
<small class="eyebrow">SIMULADOR</small>
|
||||
|
||||
<h2>
|
||||
Veja o poder dos juros compostos.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
Faça uma projeção educativa do crescimento do seu patrimônio.
|
||||
Não é recomendação de investimento.
|
||||
</p>
|
||||
|
||||
<div class="tip">
|
||||
|
||||
💡
|
||||
|
||||
<div>
|
||||
<b>Uma regra importante</b>
|
||||
|
||||
<p>
|
||||
Tempo e aportes consistentes podem fazer grande diferença
|
||||
no longo prazo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sim-card reveal delay1">
|
||||
|
||||
<form id="form">
|
||||
|
||||
<div class="inputs">
|
||||
|
||||
<label>
|
||||
Investimento inicial
|
||||
|
||||
<div class="input">
|
||||
<span>R$</span>
|
||||
<input
|
||||
id="initial"
|
||||
type="number"
|
||||
value="1000"
|
||||
min="0"
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
<label>
|
||||
Aporte mensal
|
||||
|
||||
<div class="input">
|
||||
<span>R$</span>
|
||||
<input
|
||||
id="monthly"
|
||||
type="number"
|
||||
value="300"
|
||||
min="0"
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
<label>
|
||||
Taxa mensal
|
||||
|
||||
<div class="input">
|
||||
<input
|
||||
id="rate"
|
||||
type="number"
|
||||
value="0.8"
|
||||
min="0"
|
||||
step="0.1"
|
||||
>
|
||||
<span>%</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
<label>
|
||||
Período
|
||||
|
||||
<div class="input">
|
||||
<input
|
||||
id="months"
|
||||
type="number"
|
||||
value="60"
|
||||
min="1"
|
||||
>
|
||||
<span>meses</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn primary full">
|
||||
Calcular projeção →
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<div class="results">
|
||||
|
||||
<div>
|
||||
<small>Total investido</small>
|
||||
<b id="invested">R$ 0,00</b>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<small>Rendimentos</small>
|
||||
<b id="earnings">R$ 0,00</b>
|
||||
</div>
|
||||
|
||||
<div class="highlight">
|
||||
<small>Valor projetado</small>
|
||||
<b id="future">R$ 0,00</b>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<canvas id="growth"></canvas>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section class="section" id="metas">
|
||||
|
||||
|
||||
<div class="container goal-grid">
|
||||
|
||||
<div class="heading reveal">
|
||||
|
||||
<small class="eyebrow">METAS FINANCEIRAS</small>
|
||||
|
||||
<h2>
|
||||
Transforme sonhos em números.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
Uma meta clara facilita acompanhar o progresso e manter a disciplina.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="goal-card reveal delay1">
|
||||
|
||||
<div class="goal-head">
|
||||
<b>Reserva de emergência</b>
|
||||
<strong id="percent">64%</strong>
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<span id="progress"></span>
|
||||
</div>
|
||||
|
||||
<div class="goal-values">
|
||||
<span>R$ 6.400</span>
|
||||
<span>R$ 10.000</span>
|
||||
</div>
|
||||
|
||||
<div class="goal-foot">
|
||||
<span>Faltam R$ 3.600</span>
|
||||
<button id="advance">
|
||||
Registrar progresso
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section class="quote">
|
||||
|
||||
|
||||
<div class="container reveal">
|
||||
|
||||
<span>“</span>
|
||||
|
||||
<p>
|
||||
Educação financeira não é sobre ficar rico rapidamente.
|
||||
É sobre fazer escolhas conscientes com o dinheiro que você tem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
|
||||
|
||||
<div class="container footer">
|
||||
|
||||
<div>
|
||||
|
||||
<a class="brand" href="#inicio">
|
||||
<span class="mark">F+</span>Finanças<span>+</span>
|
||||
</a>
|
||||
|
||||
<p>
|
||||
Conhecimento para decisões financeiras melhores.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="footer-links">
|
||||
|
||||
<a href="#aprenda">Aprenda</a>
|
||||
<a href="#simulador">Simulador</a>
|
||||
<a href="#metas">Metas</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<small>
|
||||
© <span id="year"></span> Finanças+. by Romerito Melo
|
||||
</small>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</footer>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/config/security.php';
|
||||
|
||||
$usuarioAutenticado = isset($_SESSION['usuario_id']);
|
||||
?>
|
||||
|
||||
<!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="Finanças+ — educação financeira e simulador de juros compostos">
|
||||
<title>Finanças+ | Educação Financeira</title>
|
||||
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<nav class="nav container">
|
||||
|
||||
|
||||
<a class="brand" href="#inicio">
|
||||
<span class="mark">F+</span>Finanças<span>+</span>
|
||||
</a>
|
||||
|
||||
<button class="menu" aria-label="Menu">☰</button>
|
||||
|
||||
<div class="links">
|
||||
<a href="#inicio">Início</a>
|
||||
<a href="#aprenda">Aprenda</a>
|
||||
<a href="#simulador">Simulador</a>
|
||||
<!-- <a href="login.php">Entrar</a> -->
|
||||
|
||||
<?php if ($usuarioAutenticado): ?>
|
||||
<a href="/dashboard.php">Dashboard</a>
|
||||
<a href="/logout.php">Sair</a>
|
||||
<?php else: ?>
|
||||
<a href="/login.php">Entrar</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<section class="hero" id="inicio">
|
||||
|
||||
<div class="orb one"></div>
|
||||
<div class="orb two"></div>
|
||||
|
||||
<div class="container hero-grid">
|
||||
|
||||
<div class="hero-copy reveal">
|
||||
|
||||
<small class="eyebrow">EDUCAÇÃO FINANCEIRA</small>
|
||||
|
||||
<h1>
|
||||
Seu dinheiro deve <em>trabalhar por você.</em>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
Aprenda a organizar suas finanças, tomar decisões melhores
|
||||
e construir um futuro financeiro mais tranquilo.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a class="btn primary" href="#simulador">
|
||||
Começar a simular →
|
||||
</a>
|
||||
|
||||
<a class="btn outline" href="#aprenda">
|
||||
Quero aprender
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="trust">
|
||||
✓ Conteúdo educativo
|
||||
✓ Simulação gratuita
|
||||
✓ Área do aluno
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="visual reveal delay1">
|
||||
|
||||
<div class="dashboard">
|
||||
|
||||
<div class="dash-top">
|
||||
<span>Patrimônio projetado</span>
|
||||
<b>●</b>
|
||||
</div>
|
||||
|
||||
<strong>R$ 84.750,00</strong>
|
||||
|
||||
<div class="mini">
|
||||
<svg viewBox="0 0 500 180" preserveAspectRatio="none">
|
||||
<path
|
||||
class="fill"
|
||||
d="M0 160 C55 150 75 130 120 138 S180 115 220 120 S270 88 310 98 S360 60 395 72 S450 30 500 22 L500 180 L0 180Z"
|
||||
/>
|
||||
<path
|
||||
class="line"
|
||||
d="M0 160 C55 150 75 130 120 138 S180 115 220 120 S270 88 310 98 S360 60 395 72 S450 30 500 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="dash-bottom">
|
||||
<span>+18,4%</span>
|
||||
<span>12 meses</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="float goal">
|
||||
🎯
|
||||
<div>
|
||||
<small>Meta mensal</small>
|
||||
<b>R$ 500</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="float saved">
|
||||
💰
|
||||
<div>
|
||||
<small>Economizado</small>
|
||||
<b>R$ 7.280</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section class="section" id="aprenda">
|
||||
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="heading reveal">
|
||||
|
||||
<small class="eyebrow">PRIMEIROS PASSOS</small>
|
||||
|
||||
<h2>
|
||||
Uma vida financeira melhor começa com três hábitos.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
Conhecimento, organização e constância fazem mais diferença
|
||||
do que procurar uma fórmula mágica.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<article class="card reveal">
|
||||
|
||||
<div class="icon">💰</div>
|
||||
|
||||
<small>01</small>
|
||||
|
||||
<h3>Organize</h3>
|
||||
|
||||
<p>
|
||||
Saiba quanto entra, quanto sai e para onde seu dinheiro está indo.
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card reveal delay1">
|
||||
|
||||
<div class="icon">📊</div>
|
||||
|
||||
<small>02</small>
|
||||
|
||||
<h3>Planeje</h3>
|
||||
|
||||
<p>
|
||||
Transforme objetivos em metas financeiras claras e mensuráveis.
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card reveal delay2">
|
||||
|
||||
<div class="icon">📈</div>
|
||||
|
||||
<small>03</small>
|
||||
|
||||
<h3>Invista</h3>
|
||||
|
||||
<p>
|
||||
Conheça as opções de investimento e o efeito dos juros compostos.
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section class="sim-section" id="simulador">
|
||||
|
||||
|
||||
<div class="container sim-grid">
|
||||
|
||||
<div class="sim-copy reveal">
|
||||
|
||||
<small class="eyebrow">SIMULADOR</small>
|
||||
|
||||
<h2>
|
||||
Veja o poder dos juros compostos.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
Faça uma projeção educativa do crescimento do seu patrimônio.
|
||||
Não é recomendação de investimento.
|
||||
</p>
|
||||
|
||||
<div class="tip">
|
||||
|
||||
💡
|
||||
|
||||
<div>
|
||||
<b>Uma regra importante</b>
|
||||
|
||||
<p>
|
||||
Tempo e aportes consistentes podem fazer grande diferença
|
||||
no longo prazo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sim-card reveal delay1">
|
||||
|
||||
<form id="form">
|
||||
|
||||
<div class="inputs">
|
||||
|
||||
<label>
|
||||
Investimento inicial
|
||||
|
||||
<div class="input">
|
||||
<span>R$</span>
|
||||
<input
|
||||
id="initial"
|
||||
type="number"
|
||||
value="1000"
|
||||
min="0"
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
<label>
|
||||
Aporte mensal
|
||||
|
||||
<div class="input">
|
||||
<span>R$</span>
|
||||
<input
|
||||
id="monthly"
|
||||
type="number"
|
||||
value="300"
|
||||
min="0"
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
<label>
|
||||
Taxa mensal
|
||||
|
||||
<div class="input">
|
||||
<input
|
||||
id="rate"
|
||||
type="number"
|
||||
value="0.8"
|
||||
min="0"
|
||||
step="0.1"
|
||||
>
|
||||
<span>%</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
<label>
|
||||
Período
|
||||
|
||||
<div class="input">
|
||||
<input
|
||||
id="months"
|
||||
type="number"
|
||||
value="60"
|
||||
min="1"
|
||||
>
|
||||
<span>meses</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn primary full">
|
||||
Calcular projeção →
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<div class="results">
|
||||
|
||||
<div>
|
||||
<small>Total investido</small>
|
||||
<b id="invested">R$ 0,00</b>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<small>Rendimentos</small>
|
||||
<b id="earnings">R$ 0,00</b>
|
||||
</div>
|
||||
|
||||
<div class="highlight">
|
||||
<small>Valor projetado</small>
|
||||
<b id="future">R$ 0,00</b>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<canvas id="growth"></canvas>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section class="section" id="metas">
|
||||
|
||||
|
||||
<div class="container goal-grid">
|
||||
|
||||
<div class="heading reveal">
|
||||
|
||||
<small class="eyebrow">METAS FINANCEIRAS</small>
|
||||
|
||||
<h2>
|
||||
Transforme sonhos em números.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
Uma meta clara facilita acompanhar o progresso e manter a disciplina.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="goal-card reveal delay1">
|
||||
|
||||
<div class="goal-head">
|
||||
<b>Reserva de emergência</b>
|
||||
<strong id="percent">64%</strong>
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<span id="progress"></span>
|
||||
</div>
|
||||
|
||||
<div class="goal-values">
|
||||
<span>R$ 6.400</span>
|
||||
<span>R$ 10.000</span>
|
||||
</div>
|
||||
|
||||
<div class="goal-foot">
|
||||
<span>Faltam R$ 3.600</span>
|
||||
<button id="advance">
|
||||
Registrar progresso
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<section class="quote">
|
||||
|
||||
|
||||
<div class="container reveal">
|
||||
|
||||
<span>“</span>
|
||||
|
||||
<p>
|
||||
Educação financeira não é sobre ficar rico rapidamente.
|
||||
É sobre fazer escolhas conscientes com o dinheiro que você tem.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
|
||||
|
||||
<div class="container footer">
|
||||
|
||||
<div>
|
||||
|
||||
<a class="brand" href="#inicio">
|
||||
<span class="mark">F+</span>Finanças<span>+</span>
|
||||
</a>
|
||||
|
||||
<p>
|
||||
Conhecimento para decisões financeiras melhores.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="footer-links">
|
||||
|
||||
<a href="#aprenda">Aprenda</a>
|
||||
<a href="#simulador">Simulador</a>
|
||||
<a href="#metas">Metas</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<small>
|
||||
© <span id="year"></span> Finanças+. by Romerito Melo
|
||||
</small>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</footer>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/config/security.php';
|
||||
|
||||
require_once __DIR__ . '/config/database.php';
|
||||
|
||||
require_once __DIR__ . '/app/Models/User.php';
|
||||
|
||||
require_once __DIR__ . '/app/Models/LoginAttempt.php';
|
||||
|
||||
require_once __DIR__ . '/app/Controllers/AuthController.php';
|
||||
|
||||
|
||||
/*
|
||||
* Cria o Model responsável pelos usuários.
|
||||
*/
|
||||
$user = new User($pdo);
|
||||
|
||||
|
||||
/*
|
||||
* Cria o Model responsável pelas tentativas
|
||||
* de autenticação.
|
||||
*/
|
||||
$loginAttempt = new LoginAttempt($pdo);
|
||||
|
||||
|
||||
/*
|
||||
* Cria o Controller responsável pelo processo
|
||||
* de autenticação.
|
||||
*
|
||||
* O Controller recebe os Models que necessita
|
||||
* para executar suas responsabilidades.
|
||||
*/
|
||||
$controller = new AuthController(
|
||||
$user,
|
||||
$loginAttempt
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Processa a requisição de login.
|
||||
*/
|
||||
$controller->login();
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/config/security.php';
|
||||
|
||||
require_once __DIR__ . '/config/database.php';
|
||||
|
||||
require_once __DIR__ . '/app/Models/User.php';
|
||||
|
||||
require_once __DIR__ . '/app/Models/LoginAttempt.php';
|
||||
|
||||
require_once __DIR__ . '/app/Controllers/AuthController.php';
|
||||
|
||||
|
||||
/*
|
||||
* Cria o Model responsável pelos usuários.
|
||||
*/
|
||||
$user = new User($pdo);
|
||||
|
||||
|
||||
/*
|
||||
* Cria o Model responsável pelas tentativas
|
||||
* de autenticação.
|
||||
*/
|
||||
$loginAttempt = new LoginAttempt($pdo);
|
||||
|
||||
|
||||
/*
|
||||
* Cria o Controller responsável pelo processo
|
||||
* de autenticação.
|
||||
*/
|
||||
$controller = new AuthController(
|
||||
$user,
|
||||
$loginAttempt
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Encerra a sessão do usuário.
|
||||
*/
|
||||
$controller->logout();
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
echo "REMOTE_ADDR: " . ($_SERVER['REMOTE_ADDR'] ?? 'N/A') . PHP_EOL;
|
||||
echo "X-Real-IP: " . ($_SERVER['HTTP_X_REAL_IP'] ?? 'N/A') . PHP_EOL;
|
||||
echo "X-Forwarded-For: " . ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? 'N/A') . PHP_EOL;
|
||||
echo "X-Forwarded-Proto: " . ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'N/A') . PHP_EOL;
|
||||
|
||||
?>
|
||||
@@ -1 +0,0 @@
|
||||
testando o push deste arquivo para a conta romeritomelo no gitea na OCI.
|
||||
Reference in New Issue
Block a user