diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php new file mode 100644 index 0000000..80cfe39 --- /dev/null +++ b/app/Controllers/AuthController.php @@ -0,0 +1,515 @@ +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; + } +} \ No newline at end of file diff --git a/app/Controllers/Controller.php b/app/Controllers/Controller.php new file mode 100644 index 0000000..d50a5fc --- /dev/null +++ b/app/Controllers/Controller.php @@ -0,0 +1,15 @@ +cat > /home/opc/html/app/Controllers/Controller.php <<'EOF' + $_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'; + } +} diff --git a/app/Controllers/SecurityController.php b/app/Controllers/SecurityController.php new file mode 100644 index 0000000..1bfdd8b --- /dev/null +++ b/app/Controllers/SecurityController.php @@ -0,0 +1,285 @@ + 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(); + } +} +?> + diff --git a/app/Models/.gitkeep b/app/Models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/Models/LoginAttempt.php b/app/Models/LoginAttempt.php new file mode 100644 index 0000000..2051370 --- /dev/null +++ b/app/Models/LoginAttempt.php @@ -0,0 +1,136 @@ +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 + ]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..fb935fd --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,96 @@ +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 + ]); + } +} diff --git a/app/Services/.gitkeep b/app/Services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/Views/auth/.gitkeep b/app/Views/auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php new file mode 100644 index 0000000..8b38a31 --- /dev/null +++ b/app/Views/auth/login.php @@ -0,0 +1,202 @@ + + + + + + + + + + + + +Acesso - Finanças+ + + + + + + + +
+ + + +
+ +
+ +
+ +
+ +
+ + + ACESSO AO SISTEMA + + + + +

+ Acesse sua área financeira. +

+ +
+ + + + + + + + + +
+ +
+ +
+ + + + + + diff --git a/app/Views/dashboard/.gitkeep b/app/Views/dashboard/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/Views/dashboard/index.php b/app/Views/dashboard/index.php new file mode 100644 index 0000000..52dbc98 --- /dev/null +++ b/app/Views/dashboard/index.php @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + +Dashboard - Finanças+ + + + + + + + +
+ + + +
+ +
+ +
+ +
+ +
+ + + ÁREA DO USUÁRIO + + +

+ Olá, ! +

+ +

+ Bem-vindo ao seu painel financeiro. +

+ +
+ +
+ +
+ +
+ ?? +
+ + + PATRIMÔNIO + + +

+ R$ 0,00 +

+ +

+ Seu patrimônio será exibido aqui. +

+ +
+ +
+ +
+ ?? +
+ + + RECEITAS + + +

+ R$ 0,00 +

+ +

+ Acompanhe suas receitas mensais. +

+ +
+ +
+ +
+ ?? +
+ + + INVESTIMENTOS + + +

+ R$ 0,00 +

+ +

+ Acompanhe a evolução dos seus investimentos. +

+ +
+ +
+ +
+ +
+ +
+ + + + + + diff --git a/app/Views/layouts/.gitkeep b/app/Views/layouts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..234ea3d --- /dev/null +++ b/config/database.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/config/security.php b/config/security.php new file mode 100644 index 0000000..e4a5019 --- /dev/null +++ b/config/security.php @@ -0,0 +1,350 @@ + 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(); +} diff --git a/css/style.css b/css/style.css index 371f050..cab5ebd 100644 --- a/css/style.css +++ b/css/style.css @@ -1 +1,357 @@ :root{--bg:#f6f8fc;--white:#fff;--text:#142238;--muted:#68778b;--blue:#2563eb;--green:#10b981;--border:#e5eaf1;--shadow:0 25px 70px #10264d16;--radius:22px;--max:1180px}*{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth}body{font-family:Inter,Arial,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}a{text-decoration:none;color:inherit}.container{width:min(var(--max),calc(100% - 40px));margin:auto}.header{position:fixed;z-index:10;top:0;width:100%;background:#ffffffd9;backdrop-filter:blur(15px);border-bottom:1px solid #e5eaf1aa}.nav{height:76px;display:flex;align-items:center;justify-content:space-between}.brand{display:flex;align-items:center;gap:9px;font-weight:800}.brand>span:last-child{color:var(--blue)}.mark{display:grid;place-items:center;width:35px;height:35px;border-radius:10px;background:var(--blue);color:#fff!important}.links{display:flex;gap:30px;color:#526177;font-size:.9rem;font-weight:600}.links a:hover{color:var(--blue)}.menu{display:none;border:0;background:none;font-size:1.5rem}.hero{min-height:760px;padding:150px 0 100px;position:relative;overflow:hidden;color:#fff;background:linear-gradient(135deg,#0a1b35,#123f79 48%,#176a72)}.orb{position:absolute;border-radius:50%;filter:blur(5px);opacity:.25}.one{width:450px;height:450px;right:-100px;top:100px;background:#38bdf8}.two{width:380px;height:380px;left:-180px;bottom:-170px;background:#22c55e}.hero-grid{position:relative;z-index:1;display:grid;grid-template-columns:1.02fr .98fr;align-items:center;gap:65px}.eyebrow{display:inline-block;color:#2d72ed;font-size:.72rem;font-weight:800;letter-spacing:.14em;margin-bottom:13px}.hero .eyebrow{color:#8bd7ff}h1{font-size:clamp(3rem,5vw,5.1rem);line-height:1.03;letter-spacing:-.055em}h1 em{font-style:normal;color:#7de2c4}.hero-copy>p{max-width:610px;margin:25px 0;color:#d4e1f0;font-size:1.08rem}.actions{display:flex;gap:13px;flex-wrap:wrap}.btn{border:0;border-radius:13px;padding:14px 21px;font:inherit;font-weight:700;cursor:pointer;display:inline-flex;justify-content:center;gap:10px;transition:.2s}.btn.primary{background:var(--blue);color:#fff;box-shadow:0 10px 30px #0b2b6640}.btn.primary:hover{transform:translateY(-2px);filter:brightness(1.08)}.btn.outline{border:1px solid #ffffff40;color:#fff}.trust{margin-top:27px;color:#bfd0e5;font-size:.76rem}.visual{min-height:450px;display:grid;place-items:center;position:relative}.dashboard{width:min(470px,100%);padding:28px;border:1px solid #ffffff25;border-radius:28px;background:#ffffff13;box-shadow:0 35px 90px #0005;backdrop-filter:blur(16px)}.dash-top,.dash-bottom{display:flex;justify-content:space-between;color:#bfd0e5;font-size:.77rem}.dash-top b{color:#4ade80}.dashboard>strong{display:block;font-size:2.35rem;margin:8px 0}.mini{height:180px}.mini svg{width:100%;height:100%}.line{fill:none;stroke:#78e4c1;stroke-width:5;stroke-linecap:round}.fill{fill:#78e4c120}.dash-bottom{color:#7de2c4}.float{position:absolute;display:flex;align-items:center;gap:11px;background:#fff;color:var(--text);padding:13px 17px;border-radius:15px;box-shadow:var(--shadow)}.float small{display:block;color:var(--muted);font-size:.65rem}.float b{display:block;font-size:.85rem}.goal{right:0;top:45px}.saved{left:0;bottom:45px}.section{padding:105px 0}.heading{max-width:720px;margin-bottom:45px}.heading h2,.sim-copy h2{font-size:clamp(2rem,3.5vw,3.1rem);line-height:1.12;letter-spacing:-.04em}.heading p,.sim-copy>p{color:var(--muted);margin-top:14px}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.card{padding:30px;background:#fff;border:1px solid var(--border);border-radius:var(--radius);transition:.25s}.card:hover{transform:translateY(-6px);box-shadow:var(--shadow)}.icon{font-size:2rem;margin-bottom:20px}.card small{color:#9aa7b7;font-weight:800;font-size:.7rem}.card h3{font-size:1.35rem;margin:5px 0}.card p{color:var(--muted);font-size:.9rem}.sim-section{padding:105px 0;background:#edf4fb}.sim-grid,.goal-grid{display:grid;grid-template-columns:.8fr 1.2fr;gap:70px;align-items:center}.tip{display:flex;gap:12px;margin-top:28px;padding:17px;background:#fff;border:1px solid var(--border);border-radius:16px}.tip p{color:var(--muted);font-size:.75rem;margin-top:3px}.sim-card{background:#fff;border:1px solid var(--border);border-radius:26px;padding:30px;box-shadow:var(--shadow)}.inputs{display:grid;grid-template-columns:1fr 1fr;gap:18px}.inputs label{font-size:.75rem;font-weight:700;color:#506076}.input{display:flex;align-items:center;margin-top:7px;padding:0 11px;border:1px solid #dce3ec;border-radius:11px;background:#fbfcfe}.input span{color:#8996a7;font-size:.74rem}.input input{width:100%;padding:12px 7px;border:0;outline:0;background:none;font:inherit;color:var(--text)}.full{width:100%;margin-top:22px}.results{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:24px}.results>div{padding:15px;border-radius:14px;background:#f5f7fa}.results small{display:block;color:var(--muted);font-size:.65rem}.results b{display:block;margin-top:4px}.results .highlight{background:#eaf8f2}.highlight b{color:#079669}.sim-card canvas{width:100%;height:190px;margin-top:15px;border-top:1px solid var(--border)}.goal-card{padding:30px;background:#fff;border:1px solid var(--border);border-radius:24px;box-shadow:var(--shadow)}.goal-head,.goal-values,.goal-foot{display:flex;justify-content:space-between;align-items:center}.goal-head strong{color:var(--blue)}.progress{height:12px;margin:20px 0 8px;background:#edf1f6;border-radius:20px;overflow:hidden}.progress span{display:block;width:64%;height:100%;border-radius:inherit;background:linear-gradient(90deg,var(--blue),#25b99a);transition:.7s}.goal-values,.goal-foot{font-size:.76rem;color:var(--muted)}.goal-foot{margin-top:24px}.goal-foot button{border:0;background:none;color:var(--blue);font-weight:700;cursor:pointer}.quote{padding:70px 0;background:#fff;text-align:center}.quote div{max-width:800px}.quote span{font-size:5rem;color:#bdd5ff;line-height:.7}.quote p{font-size:1.4rem;font-weight:600}footer{padding:50px 0;background:#0d1b30;color:#aebed1}.footer{display:grid;grid-template-columns:1.5fr 1fr auto;gap:30px;align-items:end}.footer .brand{color:#fff}.footer p{font-size:.78rem;margin-top:10px}.footer-links{display:flex;gap:22px;font-size:.8rem}.footer-links a:hover{color:#fff}footer small{font-size:.7rem}.reveal{opacity:0;transform:translateY(25px);transition:.7s}.reveal.visible{opacity:1;transform:none}.delay1{transition-delay:.12s}.delay2{transition-delay:.24s}@media(max-width:900px){.hero-grid,.sim-grid,.goal-grid{grid-template-columns:1fr}.hero{padding-top:125px}.cards{grid-template-columns:1fr}.links{display:none}.menu{display:block}.nav.open .links{display:flex;position:absolute;top:76px;left:0;right:0;background:#fff;padding:20px;flex-direction:column;gap:16px;border-bottom:1px solid var(--border)}.footer{grid-template-columns:1fr}.footer-links{order:3}}@media(max-width:560px){.container{width:calc(100% - 28px)}h1{font-size:2.8rem}.hero{min-height:auto;padding-bottom:70px}.visual{min-height:340px}.dashboard{padding:20px}.float{padding:10px 12px}.goal{right:-3px}.saved{left:-3px}.section,.sim-section{padding:75px 0}.inputs,.results{grid-template-columns:1fr}.quote p{font-size:1.15rem}} + +/* ========================================================= +LOGIN — FINANÇAS+ +========================================================= */ + +body.login-page { +min-height: 100vh; +display: flex; +flex-direction: column; +background: +radial-gradient(circle at 10% 10%, #dbeafe 0, transparent 28%), +radial-gradient(circle at 90% 90%, #d1fae5 0, transparent 25%), +var(--bg); +} + +/* Cabeçalho simplificado do login */ + +.login-header { +height: 76px; +display: flex; +align-items: center; +border-bottom: 1px solid var(--border); +background: #ffffffd9; +backdrop-filter: blur(15px); +} + +.login-header .container { +display: flex; +align-items: center; +justify-content: space-between; +} + +.login-back { +color: var(--muted); +font-size: .85rem; +font-weight: 600; +transition: .2s; +} + +.login-back:hover { +color: var(--blue); +} + +/* Área principal */ + +.login-main { +flex: 1; +display: flex; +align-items: center; +justify-content: center; +padding: 60px 20px; +} + +/* Cartão */ + +.login-card { +width: min(440px, 100%); +padding: 42px; +background: var(--white); +border: 1px solid var(--border); +border-radius: 26px; +box-shadow: var(--shadow); +} + +/* Cabeçalho do cartão */ + +.login-title { +text-align: center; +margin-bottom: 32px; +} + +.login-title .mark { +width: 48px; +height: 48px; +margin: 0 auto 18px; +border-radius: 13px; +font-size: 1rem; +} + +.login-title h1 { +font-size: 1.9rem; +line-height: 1.15; +letter-spacing: -.035em; +color: var(--text); +} + +.login-title h1 em { +font-style: normal; +color: var(--blue); +} + +.login-title p { +margin-top: 8px; +color: var(--muted); +font-size: .86rem; +} + +/* Mensagem de erro */ + +.login-error { +display: flex; +align-items: flex-start; +gap: 9px; +margin-bottom: 22px; +padding: 13px 15px; +border: 1px solid #fecaca; +border-radius: 12px; +background: #fef2f2; +color: #b91c1c; +font-size: .8rem; +line-height: 1.45; +} + +/* Campos */ + +.login-form { +display: flex; +flex-direction: column; +gap: 19px; +} + +.login-field label { +display: block; +margin-bottom: 7px; +color: #506076; +font-size: .76rem; +font-weight: 700; +} + +.login-input { +width: 100%; +padding: 13px 14px; +border: 1px solid #dce3ec; +border-radius: 11px; +outline: none; +background: #fbfcfe; +color: var(--text); +font: inherit; +font-size: .9rem; +transition: .2s; +} + +.login-input::placeholder { +color: #a5afbc; +} + +.login-input:hover { +border-color: #c7d2df; +} + +.login-input:focus { +border-color: var(--blue); +background: #fff; +box-shadow: 0 0 0 3px #2563eb18; +} + +/* Botão */ + +.login-button { +width: 100%; +margin-top: 5px; +border: 0; +border-radius: 12px; +padding: 14px 20px; +background: var(--blue); +color: #fff; +font: inherit; +font-weight: 700; +cursor: pointer; +box-shadow: 0 10px 25px #2563eb30; +transition: .2s; +} + +.login-button:hover { +transform: translateY(-2px); +filter: brightness(1.08); +} + +.login-button:active { +transform: translateY(0); +} + +/* Informação inferior */ + +.login-info { +margin-top: 25px; +padding-top: 20px; +border-top: 1px solid var(--border); +text-align: center; +color: var(--muted); +font-size: .72rem; +line-height: 1.5; +} + +.login-info strong { +color: var(--text); +} + +/* Rodapé */ + +.login-footer { +padding: 22px; +text-align: center; +color: #8b99aa; +font-size: .7rem; +} + +/* Acessibilidade */ + +.login-input:focus-visible, +.login-button:focus-visible, +.login-back:focus-visible { +outline: 3px solid #2563eb35; +outline-offset: 2px; +} + +/* ========================================================= +LOGIN — RESPONSIVO +========================================================= */ + +@media (max-width: 560px) { + +``` +.login-header { + height: 68px; +} + +.login-main { + padding: 35px 14px; + align-items: flex-start; +} + +.login-card { + margin-top: 15px; + padding: 30px 22px; + border-radius: 21px; +} + +.login-title { + margin-bottom: 27px; +} + +.login-title h1 { + font-size: 1.7rem; +} + +.login-footer { + padding: 18px 14px; +} +``` + +} + + +/* + +* ========================================================= +* TELA DE LOGIN +* ========================================================= + */ + +.login-page { +min-height: 100vh; +display: grid; +place-items: center; +padding: 40px 20px; +background: var(--bg); +} + + + +.login-card { +width: min(430px, 100%); +padding: 42px; +background: var(--white); +border: 1px solid var(--border); +border-radius: var(--radius); +box-shadow: var(--shadow); +} + + + +.login-brand { +justify-content: center; +font-size: 1.15rem; +} + +.login-heading { +margin-top: 38px; +margin-bottom: 32px; +text-align: center; +} + +.login-heading h1 { +font-size: 1.85rem; +line-height: 1.2; +letter-spacing: -0.03em; +color: var(--text); +} + +.login-heading h1 span { +color: var(--blue); +} + +.login-heading p { +margin-top: 10px; +color: var(--muted); +font-size: .85rem; +} + +.login-field { +margin-bottom: 19px; +} + +.login-field label { +display: block; +margin-bottom: 7px; +color: var(--text); +font-size: .78rem; +font-weight: 700; +} + +.login-field input { +width: 100%; +padding: 13px 14px; +border: 1px solid #dce3ec; +border-radius: 11px; +outline: none; +background: #fbfcfe; +color: var(--text); +font: inherit; +transition: .2s; +} + +.login-field input:focus { +border-color: var(--blue); +box-shadow: 0 0 0 3px #2563eb18; +} + + +.login-back { +display: block; +margin-top: 25px; +color: var(--muted); +text-align: center; +font-size: .78rem; +font-weight: 600; +} + +.login-back:hover { +color: var(--blue); +} + +@media (max-width: 560px) { + +} diff --git a/dashboard.php b/dashboard.php new file mode 100644 index 0000000..6d4251b --- /dev/null +++ b/dashboard.php @@ -0,0 +1,10 @@ +index(); diff --git a/includes/auth.php b/includes/auth.php new file mode 100644 index 0000000..4be6b67 --- /dev/null +++ b/includes/auth.php @@ -0,0 +1,21 @@ + +Finanças+ | Educação Financeira -
-
EDUCAÇÃO FINANCEIRA

Seu dinheiro deve trabalhar por você.

Aprenda a organizar suas finanças, tomar decisões melhores e construir um futuro financeiro mais tranquilo.

✓ Conteúdo educativo   ✓ Simulação gratuita   ✓ Sem cadastro
Patrimônio projetado●
R$ 84.750,00
+18,4%12 meses
🎯
Meta mensalR$ 500
💰
EconomizadoR$ 7.280
-
PRIMEIROS PASSOS

Uma vida financeira melhor começa com três hábitos.

Conhecimento, organização e constância fazem mais diferença do que procurar uma fórmula mágica.

💰
01

Organize

Saiba quanto entra, quanto sai e para onde seu dinheiro está indo.

📊
02

Planeje

Transforme objetivos em metas financeiras claras e mensuráveis.

📈
03

Invista

Conheça as opções de investimento e o efeito dos juros compostos.

-
SIMULADOR

Veja o poder dos juros compostos.

Faça uma projeção educativa do crescimento do seu patrimônio. Não é recomendação de investimento.

💡
Uma regra importante

Tempo e aportes consistentes podem fazer grande diferença no longo prazo.

Total investidoR$ 0,00
RendimentosR$ 0,00
Valor projetadoR$ 0,00
-
METAS FINANCEIRAS

Transforme sonhos em números.

Uma meta clara facilita acompanhar o progresso e manter a disciplina.

Reserva de emergência64%
R$ 6.400R$ 10.000
Faltam R$ 3.600
“

Educação financeira não é sobre ficar rico rapidamente. É sobre fazer escolhas conscientes com o dinheiro que você tem.

- + + + + + + + + Finanças+ | Educação Financeira + + + + + + + + + + + + + +
+ + + +
+ +
+ +
+ +
+
+ +
+ +
+ + EDUCAÇÃO FINANCEIRA + +

+ Seu dinheiro deve trabalhar por você. +

+ +

+ Aprenda a organizar suas finanças, tomar decisões melhores + e construir um futuro financeiro mais tranquilo. +

+ + + +
+ ✓ Conteúdo educativo   + ✓ Simulação gratuita   + ✓ Área do aluno +
+ +
+ +
+ +
+ +
+ Patrimônio projetado + ● +
+ + R$ 84.750,00 + +
+ + + + +
+ +
+ +18,4% + 12 meses +
+ +
+ +
+ 🎯 +
+ Meta mensal + R$ 500 +
+
+ +
+ 💰 +
+ Economizado + R$ 7.280 +
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + PRIMEIROS PASSOS + +

+ Uma vida financeira melhor começa com três hábitos. +

+ +

+ Conhecimento, organização e constância fazem mais diferença + do que procurar uma fórmula mágica. +

+ +
+ +
+ +
+ +
💰
+ + 01 + +

Organize

+ +

+ Saiba quanto entra, quanto sai e para onde seu dinheiro está indo. +

+ +
+ +
+ +
📊
+ + 02 + +

Planeje

+ +

+ Transforme objetivos em metas financeiras claras e mensuráveis. +

+ +
+ +
+ +
📈
+ + 03 + +

Invista

+ +

+ Conheça as opções de investimento e o efeito dos juros compostos. +

+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + SIMULADOR + +

+ Veja o poder dos juros compostos. +

+ +

+ Faça uma projeção educativa do crescimento do seu patrimônio. + Não é recomendação de investimento. +

+ +
+ + 💡 + +
+ Uma regra importante + +

+ Tempo e aportes consistentes podem fazer grande diferença + no longo prazo. +

+
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ + + +
+ + +
+ +
+ Total investido + R$ 0,00 +
+ +
+ Rendimentos + R$ 0,00 +
+ +
+ Valor projetado + R$ 0,00 +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + METAS FINANCEIRAS + +

+ Transforme sonhos em números. +

+ +

+ Uma meta clara facilita acompanhar o progresso e manter a disciplina. +

+ +
+ + +
+ +
+ Reserva de emergência + 64% +
+ +
+ +
+ +
+ R$ 6.400 + R$ 10.000 +
+ +
+ Faltam R$ 3.600 + +
+ +
+ +
+ + +
+ +
+ + +
+ + “ + +

+ Educação financeira não é sobre ficar rico rapidamente. + É sobre fazer escolhas conscientes com o dinheiro que você tem. +

+ +
+ + +
+ +
+ + + + + + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..81872a6 --- /dev/null +++ b/index.php @@ -0,0 +1,472 @@ + + + + + + + + + + Finanças+ | Educação Financeira + + + + + + + + + + + + + +
+ + + +
+ +
+ +
+ +
+
+ +
+ +
+ + EDUCAÇÃO FINANCEIRA + +

+ Seu dinheiro deve trabalhar por você. +

+ +

+ Aprenda a organizar suas finanças, tomar decisões melhores + e construir um futuro financeiro mais tranquilo. +

+ + + +
+ ✓ Conteúdo educativo   + ✓ Simulação gratuita   + ✓ Área do aluno +
+ +
+ +
+ +
+ +
+ Patrimônio projetado + ● +
+ + R$ 84.750,00 + +
+ + + + +
+ +
+ +18,4% + 12 meses +
+ +
+ +
+ 🎯 +
+ Meta mensal + R$ 500 +
+
+ +
+ 💰 +
+ Economizado + R$ 7.280 +
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + PRIMEIROS PASSOS + +

+ Uma vida financeira melhor começa com três hábitos. +

+ +

+ Conhecimento, organização e constância fazem mais diferença + do que procurar uma fórmula mágica. +

+ +
+ +
+ +
+ +
💰
+ + 01 + +

Organize

+ +

+ Saiba quanto entra, quanto sai e para onde seu dinheiro está indo. +

+ +
+ +
+ +
📊
+ + 02 + +

Planeje

+ +

+ Transforme objetivos em metas financeiras claras e mensuráveis. +

+ +
+ +
+ +
📈
+ + 03 + +

Invista

+ +

+ Conheça as opções de investimento e o efeito dos juros compostos. +

+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + SIMULADOR + +

+ Veja o poder dos juros compostos. +

+ +

+ Faça uma projeção educativa do crescimento do seu patrimônio. + Não é recomendação de investimento. +

+ +
+ + 💡 + +
+ Uma regra importante + +

+ Tempo e aportes consistentes podem fazer grande diferença + no longo prazo. +

+
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ + + +
+ + +
+ +
+ Total investido + R$ 0,00 +
+ +
+ Rendimentos + R$ 0,00 +
+ +
+ Valor projetado + R$ 0,00 +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + METAS FINANCEIRAS + +

+ Transforme sonhos em números. +

+ +

+ Uma meta clara facilita acompanhar o progresso e manter a disciplina. +

+ +
+ + +
+ +
+ Reserva de emergência + 64% +
+ +
+ +
+ +
+ R$ 6.400 + R$ 10.000 +
+ +
+ Faltam R$ 3.600 + +
+ +
+ +
+ + +
+ +
+ + +
+ + “ + +

+ Educação financeira não é sobre ficar rico rapidamente. + É sobre fazer escolhas conscientes com o dinheiro que você tem. +

+ +
+ + +
+ +
+ + + + + + + diff --git a/login.php b/login.php new file mode 100644 index 0000000..7576e03 --- /dev/null +++ b/login.php @@ -0,0 +1,45 @@ +login(); diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..f2cd26b --- /dev/null +++ b/logout.php @@ -0,0 +1,42 @@ +logout(); diff --git a/storage/cache/.gitkeep b/storage/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/storage/logs/.gitkeep b/storage/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test-ip.php b/test-ip.php new file mode 100644 index 0000000..3af43c3 --- /dev/null +++ b/test-ip.php @@ -0,0 +1,10 @@ + diff --git a/teste_de_push.txt b/teste_de_push.txt deleted file mode 100644 index 56a68b5..0000000 --- a/teste_de_push.txt +++ /dev/null @@ -1 +0,0 @@ -testando o push deste arquivo para a conta romeritomelo no gitea na OCI. \ No newline at end of file