diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0fbc6d2..45d7a35 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,38 +1,26 @@ -# Nome do seu workflow name: Deploy to InfinityFree on: push: branches: - - main # Mude para 'master' se for o nome da sua branch principal + - main jobs: ftp-deploy: - # O tipo de máquina que o workflow vai usar runs-on: ubuntu-latest steps: - # 1. Baixa o código do seu repositório para a máquina virtual - name: Check out code uses: actions/checkout@v4 - # --- NOVOS PASSOS ADICIONADOS AQUI --- - - # 1.1 Prepara o ambiente PHP na máquina do GitHub - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' # Importante: Use a mesma versão que configurou no InfinityFree (geralmente 8.1 ou 8.2) + php-version: '8.2' - # 1.2 Roda o composer para criar a pasta 'vendor' - name: Install Dependencies run: composer install --no-dev --optimize-autoloader - # --no-dev: Não baixa ferramentas de teste (economiza espaço/inodes no InfinityFree) - # --optimize-autoloader: Deixa o carregamento das classes mais rápido - - # ------------------------------------- - # 2. Executa a ação de deploy via FTP - name: FTP Deploy uses: SamKirkland/FTP-Deploy-Action@v4.3.4 with: @@ -40,9 +28,11 @@ jobs: username: ${{ secrets.FTP_USERNAME }} password: ${{ secrets.FTP_PASSWORD }} server-dir: /htdocs/ - - # Nota: A ação vai pegar a pasta 'vendor' criada no passo anterior e enviar junto exclude: | **/.git* **/.git*/** **/node_modules/** + config.php + .env + logs/** + tests/** \ No newline at end of file diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php index 21d7831..8f318fc 100644 --- a/app/Controllers/AuthController.php +++ b/app/Controllers/AuthController.php @@ -12,13 +12,13 @@ class AuthController { - //Exibe o formulário de login + // Exibe o formulário de login public function index() { ViewHelper::render('auth/login'); } - //Processa login do usuairo + // Processa login do usuário public function authenticate() { if ($_SERVER['REQUEST_METHOD'] === 'POST') { @@ -27,44 +27,44 @@ public function authenticate() $user = User::findByEmail($email); - if ($user && password_verify($password, $user['password_hash'])) { - // Troca o ID da sessão para um novo e deleta o antigo. - // Isso impede que alguém use um cookie antigo roubado. + // Ajustado de 'password_hash' para 'password' conforme o novo SQL + if ($user && password_verify($password, $user['password'])) { + + // Impede fixação de sessão usando cookie antigo session_regenerate_id(true); - //Inicia a sessão + + // Inicia a sessão $_SESSION['user_id'] = $user['id']; $_SESSION['user_name'] = $user['name']; $_SESSION['user_role'] = $user['role']; $_SESSION['user_avatar'] = $user['avatar_path']; - //"Lembrar de mim" + // "Lembrar de mim" usando a nova coluna 'remember_token' da tabela 'users' if (isset($_POST['remember-me'])) { - $selector = bin2hex(random_bytes(16)); - $validator = bin2hex(random_bytes(32)); - $expiry = date('Y-m-d H:i:s', time() + (30 * 24 * 60 * 60)); // 30 dias - $hashedValidator = hash('sha256', $validator); + $rememberToken = bin2hex(random_bytes(32)); // Gera um token seguro de 64 caracteres try { $conn = \App\Core\Database::getConnection(); - $sql = "INSERT INTO user_tokens (user_id, selector, hashed_validator, expiry) VALUES (?, ?, ?, ?)"; + // Atualiza o token diretamente na linha do usuário + $sql = "UPDATE users SET remember_token = ? WHERE id = ?"; $stmt = $conn->prepare($sql); - $stmt->execute([$user['id'], $selector, $hashedValidator, $expiry]); + $stmt->execute([$rememberToken, $user['id']]); setcookie( 'remember_me', - $selector . ':' . $validator, - time() + (30 * 24 * 60 * 60), + $rememberToken, + time() + (30 * 24 * 60 * 60), // 30 dias '/', '', - false, //Mudar para true em produção com HTTPS - true // HttpOnly + false, // Mudar para true em produção com HTTPS + true // HttpOnly ); } catch (\Exception $e) { - error_log("Erro ao criar token remember me: " . $e->getMessage()); + error_log("Erro ao salvar remember_token: " . $e->getMessage()); } } - //Redireciona + // Redireciona dependendo do nível de acesso $redirect_to = ($user['role'] === 'admin') ? '/admin' : '/dashboard'; header('Location: ' . BASE_URL . $redirect_to); exit; @@ -75,18 +75,18 @@ public function authenticate() } } - //Exibe o formulário de registro + // Exibe o formulário de registro public function register() { ViewHelper::render('auth/register'); } - //Processa o registro de um novo usuário + // Processa o registro de um novo usuário public function store() { if ($_SERVER['REQUEST_METHOD'] === 'POST') { - //Verifica se 'password' é igual a 'password_confirmation' + // Verifica se 'password' é igual a 'password_confirmation' if ($_POST['password'] !== $_POST['password_confirmation']) { header('Location: ' . BASE_URL . '/register?error=password_mismatch'); exit; @@ -102,12 +102,14 @@ public function store() exit; } + // DADOS DO USUÁRIO ADAPTADOS: + // Campo 'password' corrigido e 'role' alterado para 'player' (atendendo ao ENUM do banco) $userData = [ 'name' => $_POST['name'], 'email' => $_POST['email'], 'birthdate' => $_POST['birthdate'], - 'password_hash' => password_hash($_POST['password'], PASSWORD_DEFAULT), - 'role' => 'user' + 'password' => password_hash($_POST['password'], PASSWORD_DEFAULT), + 'role' => 'player' ]; if (User::create($userData)) { @@ -120,13 +122,13 @@ public function store() } } - //Exibe o formulário "Esqueci minha senha" + // Exibe o formulário "Esqueci minha senha" public function showForgotPasswordForm() { ViewHelper::render('auth/forgot-password'); } - //Processa a solicitação de redefinição de senha e envia o e-mail + // Processa a solicitação de redefinição de senha e envia o e-mail public function handleForgotPassword() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { @@ -145,10 +147,10 @@ public function handleForgotPassword() if ($user) { try { $token = bin2hex(random_bytes(32)); - $expires_at = (new DateTime('+1 hour'))->format('Y-m-d H:i:s'); + // Removido o parâmetro $expires_at pois a tabela 'password_reset_tokens' só possui 'created_at' PasswordReset::deleteTokensForEmail($email); - PasswordReset::createToken($email, $token, $expires_at); + PasswordReset::createToken($email, $token); $mail = new PHPMailer(true); $mail->SMTPDebug = 2; @@ -198,7 +200,7 @@ public function showResetForm() ViewHelper::render('auth/reset-password', ['token' => $token]); } - //Processa a submissão da nova senha + // Processa a submissão da nova senha public function handleResetPassword() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { @@ -234,23 +236,19 @@ public function handleResetPassword() } } - //Processa o logout do usuário + // Processa o logout do usuário public function logout() { + // Limpa o remember_token do banco antes de destruir o cookie if (isset($_COOKIE['remember_me'])) { - $parts = explode(':', $_COOKIE['remember_me']); - - if (count($parts) === 2) { - list($selector,) = $parts; - try { - // Usa namespace completo para evitar erros - $conn = \App\Core\Database::getConnection(); - $sql = "DELETE FROM user_tokens WHERE selector = ?"; - $stmt = $conn->prepare($sql); - $stmt->execute([$selector]); - } catch (\Exception $e) { - error_log("Erro ao limpar token de logout: " . $e->getMessage()); - } + $token = $_COOKIE['remember_me']; + try { + $conn = \App\Core\Database::getConnection(); + $sql = "UPDATE users SET remember_token = NULL WHERE remember_token = ?"; + $stmt = $conn->prepare($sql); + $stmt->execute([$token]); + } catch (\Exception $e) { + error_log("Erro ao limpar token de logout: " . $e->getMessage()); } setcookie('remember_me', '', time() - 3600, '/'); unset($_COOKIE['remember_me']); @@ -276,7 +274,7 @@ public function logout() exit; } - //Função para verificar maioridade + // Função para verificar maioridade private function isOver18($birthdate): bool { if (empty($birthdate)) return false; @@ -289,4 +287,4 @@ private function isOver18($birthdate): bool return false; } } -} +} \ No newline at end of file diff --git a/app/Controllers/UserController.php b/app/Controllers/UserController.php index f446590..d4ad4ad 100644 --- a/app/Controllers/UserController.php +++ b/app/Controllers/UserController.php @@ -8,11 +8,8 @@ use App\Models\User; use App\Models\Venue; - class UserController { - - public function dashboard() { AuthHelper::check(); @@ -22,7 +19,6 @@ public function dashboard() $data = [ 'userName' => $_SESSION['user_name'] ?? 'Usuario', 'userVenues' => Venue::findByUserId($userId), - 'showCnpjModal' => empty($user['cnpj']) ]; @@ -39,8 +35,6 @@ public function profile() ViewHelper::render('users/profile', $data); } - - public function updateProfile() { AuthHelper::check(); @@ -52,7 +46,6 @@ public function updateProfile() $userId = $_SESSION['user_id']; $newAvatarFileName = null; // Variável para guardar o nome do novo arquivo - if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) { // Define o diretório CORRETO @@ -68,7 +61,6 @@ public function updateProfile() } } - $updateData = [ 'name' => htmlspecialchars(trim($_POST['name'] ?? '')) // A data de nascimento é desabilitada no form, então não precisamos enviá-la @@ -118,14 +110,13 @@ public function updatePasswordFromProfile() $newPassword = $_POST['new_password'] ?? ''; $confirmPassword = $_POST['confirm_password'] ?? ''; - $user = User::findById($userId); if (!$user) { header('Location: ' . BASE_URL . '/dashboard/perfil/seguranca?error=update_failed'); exit; } - if (!password_verify($currentPassword, $user['password_hash'])) { + if (!password_verify($currentPassword, $user['password'])) { header('Location: ' . BASE_URL . '/dashboard/perfil/seguranca?error=current_mismatch'); exit; } @@ -140,12 +131,9 @@ public function updatePasswordFromProfile() exit; } - - // Se todas as validações passaram, hash a nova senha $newPasswordHash = password_hash($newPassword, PASSWORD_DEFAULT); - // Tenta atualizar no banco de dados - if (User::update($userId, ['password_hash' => $newPasswordHash])) { + if (User::update($userId, ['password' => $newPasswordHash])) { // Sucesso! Redireciona com status=success header('Location: ' . BASE_URL . '/dashboard/perfil/seguranca?status=success'); } else { @@ -174,12 +162,10 @@ public function storeCnpj() $userId = $_SESSION['user_id']; if (!$this->validateCnpj($cnpj)) { - // Se o CNPJ for inválido, redireciona de volta com um erro para o popup. header('Location: ' . BASE_URL . '/dashboard?error=cnpj_invalid'); exit; } if (User::isCnpjInUse($cnpj, $userId)) { - // Se o CNPJ já estiver em uso, redireciona com outro erro. header('Location: ' . BASE_URL . '/dashboard?error=cnpj_in_use'); exit; } @@ -188,12 +174,10 @@ public function storeCnpj() exit; } } - // Se ocorrer um erro geral, redireciona. header('Location: ' . BASE_URL . '/dashboard?error=generic'); exit; } - // --- MÉTODOS DA ÁREA DO ADMINISTRADOR --- public function index() @@ -217,12 +201,13 @@ public function store() AuthHelper::checkAdmin(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $randomPassword = bin2hex(random_bytes(4)); + $data = [ 'name' => trim($_POST['name']), 'email' => trim($_POST['email']), 'birthdate' => $_POST['birthdate'], - 'role' => in_array($_POST['role'], ['user', 'admin']) ? $_POST['role'] : 'user', - 'password_hash' => password_hash($randomPassword, PASSWORD_DEFAULT) + 'role' => in_array($_POST['role'], ['admin', 'owner', 'player']) ? $_POST['role'] : 'player', + 'password' => password_hash($randomPassword, PASSWORD_DEFAULT) ]; if (User::create($data)) { $_SESSION['flash_message'] = ['type' => 'success_with_password', 'password' => $randomPassword]; @@ -255,11 +240,14 @@ public function update() AuthHelper::checkAdmin(); if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) { $id = (int)$_POST['id']; + + $role = in_array($_POST['role'], ['admin', 'owner', 'player']) ? $_POST['role'] : 'player'; + $data = [ 'name' => trim($_POST['name']), 'email' => trim($_POST['email']), 'birthdate' => $_POST['birthdate'], - 'role' => $_POST['role'] + 'role' => $role ]; if (User::update($id, $data)) { header('Location: ' . BASE_URL . '/admin/usuarios?status=updated'); @@ -312,4 +300,4 @@ private function validateCnpj(string $cnpj): bool } return true; } -} +} \ No newline at end of file diff --git a/app/Models/Address.php b/app/Models/Address.php index 2d24bd7..8040d83 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -7,114 +7,229 @@ /** * Class Address - * Gerencia operações de banco de dados para endereços com Geocodificação. + * Gerencia operações de banco de dados para endereços com relacionamentos hierárquicos e Geocodificação. */ class Address { /** - * Cria um novo endereço e busca coordenadas automaticamente. + * Cria um novo endereço lidando com as tabelas relacionais (states, cities, neighborhoods, streets) + * e busca coordenadas automaticamente. */ public static function create(array $data) { - // Busca coordenadas antes de salvar - $coords = self::getCoordinates($data); - $latitude = $coords['lat'] ?? null; - $longitude = $coords['lng'] ?? null; - $pdo = Database::getConnection(); - $query = "INSERT INTO addresses (cep, street, number, neighborhood, complement, city, state, latitude, longitude) - VALUES (:cep, :street, :number, :neighborhood, :complement, :city, :state, :latitude, :longitude)"; + try { + // Inicia uma transação para garantir que tudo seja salvo ou nada + $pdo->beginTransaction(); - $stmt = $pdo->prepare($query); + // 1. Resolve a hierarquia geográfica para obter o street_id + $streetId = self::resolveAddressHierarchy($pdo, $data); - $params = [ - ':cep' => $data['cep'], - ':street' => $data['street'], - ':number' => $data['number'], - ':neighborhood' => $data['neighborhood'], - ':complement' => $data['complement'] ?? null, - ':city' => $data['city'], - ':state' => $data['state'], - ':latitude' => $latitude, - ':longitude' => $longitude - ]; - - if ($stmt->execute($params)) { - return $pdo->lastInsertId(); + if (!$streetId) { + throw new \Exception("Falha ao resolver a hierarquia de endereço."); + } + + // 2. Busca coordenadas + $coords = self::getCoordinates($data); + $latitude = $coords['lat'] ?? null; + $longitude = $coords['lng'] ?? null; + + // 3. Insere na tabela de endereços final + $query = "INSERT INTO addresses (street_id, cep, number, complement, latitude, longitude) + VALUES (:street_id, :cep, :number, :complement, :latitude, :longitude)"; + + $stmt = $pdo->prepare($query); + + $params = [ + ':street_id' => $streetId, + ':cep' => $data['cep'] ?? null, + ':number' => $data['number'] ?? null, + ':complement' => $data['complement'] ?? null, + ':latitude' => $latitude, + ':longitude' => $longitude + ]; + + if ($stmt->execute($params)) { + $addressId = $pdo->lastInsertId(); + $pdo->commit(); + return $addressId; + } + + $pdo->rollBack(); + return false; + + } catch (\Exception $e) { + $pdo->rollBack(); + error_log("Erro ao salvar endereço: " . $e->getMessage()); + return false; } - return false; } /** - * Atualiza um endereço e RECALCULA as coordenadas. + * Atualiza um endereço existente. */ public static function update(int $id, array $data): bool { - // Recalcula coordenadas pois o endereço mudou - $coords = self::getCoordinates($data); - $latitude = $coords['lat'] ?? null; - $longitude = $coords['lng'] ?? null; - $pdo = Database::getConnection(); - $query = "UPDATE addresses SET - cep = :cep, - street = :street, - number = :number, - neighborhood = :neighborhood, - complement = :complement, - city = :city, - state = :state, - latitude = :latitude, - longitude = :longitude - WHERE id = :id"; + try { + $pdo->beginTransaction(); - $stmt = $pdo->prepare($query); + // 1. Resolve a hierarquia para ver se a rua/cidade/etc mudou e pega o novo street_id + $streetId = self::resolveAddressHierarchy($pdo, $data); - $params = [ - ':cep' => $data['cep'], - ':street' => $data['street'], - ':number' => $data['number'], - ':neighborhood' => $data['neighborhood'], - ':complement' => $data['complement'] ?? null, - ':city' => $data['city'], - ':state' => $data['state'], - ':latitude' => $latitude, - ':longitude' => $longitude, - ':id' => $id - ]; - - return $stmt->execute($params); + if (!$streetId) { + throw new \Exception("Falha ao resolver a hierarquia de endereço."); + } + + // 2. Recalcula coordenadas pois o endereço pode ter mudado + $coords = self::getCoordinates($data); + $latitude = $coords['lat'] ?? null; + $longitude = $coords['lng'] ?? null; + + // 3. Atualiza a tabela addresses + $query = "UPDATE addresses SET + street_id = :street_id, + cep = :cep, + number = :number, + complement = :complement, + latitude = :latitude, + longitude = :longitude + WHERE id = :id"; + + $stmt = $pdo->prepare($query); + + $params = [ + ':street_id' => $streetId, + ':cep' => $data['cep'] ?? null, + ':number' => $data['number'] ?? null, + ':complement' => $data['complement'] ?? null, + ':latitude' => $latitude, + ':longitude' => $longitude, + ':id' => $id + ]; + + $result = $stmt->execute($params); + $pdo->commit(); + return $result; + + } catch (\Exception $e) { + $pdo->rollBack(); + error_log("Erro ao atualizar endereço: " . $e->getMessage()); + return false; + } } public static function findById(int $id) { $pdo = Database::getConnection(); - $query = "SELECT * FROM addresses WHERE id = :id"; + // Faz JOIN com todas as tabelas para retornar os nomes reais ao invés de apenas IDs + $query = "SELECT a.id, a.cep, a.number, a.complement, a.latitude, a.longitude, + s.name AS street, n.name AS neighborhood, c.name AS city, st.name AS state, co.name AS country + FROM addresses a + JOIN streets s ON a.street_id = s.id + JOIN neighborhoods n ON s.neighborhood_id = n.id + JOIN cities c ON n.city_id = c.id + JOIN states st ON c.state_id = st.id + JOIN countries co ON st.country_id = co.id + WHERE a.id = :id"; + $stmt = $pdo->prepare($query); $stmt->bindParam(':id', $id); $stmt->execute(); return $stmt->fetch(PDO::FETCH_ASSOC); } + /** + * Lida com a inserção hierárquica (Country -> State -> City -> Neighborhood -> Street) + * Retorna o ID da Street para ser usado na tabela Address. + */ + private static function resolveAddressHierarchy(PDO $pdo, array $data): ?int + { + // 1. Country (Assumindo 'Brasil' como padrão se não for fornecido, já que as Cidades costumam vir do ViaCEP) + $countryName = $data['country'] ?? 'Brasil'; + $countryId = self::findOrCreate($pdo, 'countries', ['name' => $countryName]); + + // 2. State + $stateName = $data['state'] ?? 'Desconhecido'; + $stateId = self::findOrCreate($pdo, 'states', ['name' => $stateName, 'country_id' => $countryId]); + + // 3. City + $cityName = $data['city'] ?? 'Desconhecido'; + $cityId = self::findOrCreate($pdo, 'cities', ['name' => $cityName, 'state_id' => $stateId]); + + // 4. Neighborhood + $neighborhoodName = $data['neighborhood'] ?? 'Desconhecido'; + $neighborhoodId = self::findOrCreate($pdo, 'neighborhoods', ['name' => $neighborhoodName, 'city_id' => $cityId]); + + // 5. Street + $streetName = $data['street'] ?? 'Desconhecido'; + $streetId = self::findOrCreate($pdo, 'streets', ['name' => $streetName, 'neighborhood_id' => $neighborhoodId]); + + return $streetId; + } + + /** + * Busca um registro ou o cria se não existir. Retorna o ID. + */ + private static function findOrCreate(PDO $pdo, string $table, array $conditions): ?int + { + // Constrói a query de SELECT + $whereClauses = []; + $params = []; + foreach ($conditions as $key => $value) { + $whereClauses[] = "$key = :$key"; + $params[":$key"] = $value; + } + $whereSql = implode(' AND ', $whereClauses); + + $selectQuery = "SELECT id FROM $table WHERE $whereSql LIMIT 1"; + $stmt = $pdo->prepare($selectQuery); + $stmt->execute($params); + $id = $stmt->fetchColumn(); + + if ($id) { + return (int)$id; // Retorna se já existe + } + + // Se não existe, faz o INSERT + $insertColumns = implode(', ', array_keys($conditions)); + $insertPlaceholders = implode(', ', array_keys($params)); + + $insertQuery = "INSERT INTO $table ($insertColumns) VALUES ($insertPlaceholders)"; + $insertStmt = $pdo->prepare($insertQuery); + + if ($insertStmt->execute($params)) { + return (int)$pdo->lastInsertId(); + } + + return null; // Falha na inserção + } + /** * Método auxiliar privado para obter Latitude e Longitude. */ private static function getCoordinates(array $data): ?array { - // Verifica se a chave está definida e não está vazia if (!defined('GOOGLE_MAPS_API_KEY') || empty(GOOGLE_MAPS_API_KEY)) { return null; } - // Monta string do endereço - $addressString = "{$data['street']}, {$data['number']} - {$data['neighborhood']}, {$data['city']} - {$data['state']}, {$data['cep']}"; - $fullAddress = urlencode($addressString); + $street = $data['street'] ?? ''; + $number = $data['number'] ?? ''; + $neighborhood = $data['neighborhood'] ?? ''; + $city = $data['city'] ?? ''; + $state = $data['state'] ?? ''; + $cep = $data['cep'] ?? ''; + + $addressString = "{$street}, {$number} - {$neighborhood}, {$city} - {$state}, {$cep}"; + $fullAddress = urlencode(trim($addressString, " ,-")); + + if (empty($fullAddress)) return null; $url = "https://maps.googleapis.com/maps/api/geocode/json?address={$fullAddress}&key=" . GOOGLE_MAPS_API_KEY; - // Usa @ para suprimir warnings, mas idealmente usar cURL em produção $responseJson = @file_get_contents($url); if ($responseJson) { @@ -130,4 +245,4 @@ private static function getCoordinates(array $data): ?array return null; } -} +} \ No newline at end of file diff --git a/app/Models/PasswordReset.php b/app/Models/PasswordReset.php index 3b24ba3..facd456 100644 --- a/app/Models/PasswordReset.php +++ b/app/Models/PasswordReset.php @@ -8,12 +8,14 @@ class PasswordReset { /** - * Encontra um token válido que ainda não expirou. + * Encontra um token válido que ainda não expirou (válido por 1 hora). */ public static function findValidToken($token) { $pdo = Database::getConnection(); - $stmt = $pdo->prepare("SELECT * FROM password_resets WHERE token = ? AND expires_at > NOW()"); + + // Verifica se o token existe e se foi criado há menos de 1 hora + $stmt = $pdo->prepare("SELECT * FROM password_reset_tokens WHERE token = ? AND created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)"); $stmt->execute([$token]); return $stmt->fetch(PDO::FETCH_ASSOC); @@ -21,15 +23,17 @@ public static function findValidToken($token) /** * Cria um novo registro de token de reset. + * Agora recebe apenas email e token, já que o created_at é automático no banco. */ - public static function createToken($email, $token, $expires_at) + public static function createToken($email, $token) { $pdo = Database::getConnection(); try { - $stmt = $pdo->prepare("INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, ?)"); - return $stmt->execute([$email, $token, $expires_at]); + $stmt = $pdo->prepare("INSERT INTO password_reset_tokens (email, token) VALUES (?, ?)"); + return $stmt->execute([$email, $token]); } catch (\Exception $e) { + error_log("Erro ao criar token de reset: " . $e->getMessage()); return false; } } @@ -41,7 +45,7 @@ public static function createToken($email, $token, $expires_at) public static function deleteTokensForEmail($email) { $pdo = Database::getConnection(); - $stmt = $pdo->prepare("DELETE FROM password_resets WHERE email = ?"); + $stmt = $pdo->prepare("DELETE FROM password_reset_tokens WHERE email = ?"); return $stmt->execute([$email]); } -} +} \ No newline at end of file diff --git a/app/Models/Sport.php b/app/Models/Sport.php index 7c05d73..7dde80f 100644 --- a/app/Models/Sport.php +++ b/app/Models/Sport.php @@ -53,8 +53,8 @@ public static function findById(int $id) return $stmt->fetch(PDO::FETCH_ASSOC); } - /** - * Cria um novo esportes no banco de dados. + /** + * Cria um novo esporte no banco de dados. * @param array $data * @return bool */ @@ -62,19 +62,34 @@ public static function create(array $data): bool { $pdo = Database::getConnection(); $data['status'] = 'active'; - $query = "INSERT INTO sports (name, icon, status) VALUES (:name, :icon, :status)"; + + // Verificação de segurança: se o Controller ainda estiver enviando a chave 'icon', + // nós a renomeamos para 'icon_path' antes de executar no banco. + if (isset($data['icon'])) { + $data['icon_path'] = $data['icon']; + unset($data['icon']); + } + + // Atualizado de 'icon' para 'icon_path' + $query = "INSERT INTO sports (name, icon_path, status) VALUES (:name, :icon_path, :status)"; $stmt = $pdo->prepare($query); return $stmt->execute($data); } - /** - * Atualiza um esportes existente. + /** + * Atualiza um esporte existente. * @param int $id * @param array $data * @return bool */ public static function update(int $id, array $data): bool { + // Renomeia 'icon' para 'icon_path' caso o Controller ainda envie o nome antigo + if (isset($data['icon'])) { + $data['icon_path'] = $data['icon']; + unset($data['icon']); + } + if (empty($data)) { return true; } @@ -92,7 +107,7 @@ public static function update(int $id, array $data): bool $data['id'] = $id; return $stmt->execute($data); } - + /** * Realiza um "soft delete" de um esportes. * @param int $id diff --git a/app/Models/User.php b/app/Models/User.php index c120496..911c1ec 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,19 +5,12 @@ use App\Core\Database; use PDO; -/** - * Class User - * Gerencia todas as operações de banco de dados para a entidade de utilizador. - */ class User { - // --- MÉTODOS DE CONTAGEM E BUSCA RECENTE --- - public static function countAll(): int { $pdo = Database::getConnection(); - $query = "SELECT COUNT(id) FROM users WHERE status = 'active'"; - $stmt = $pdo->prepare($query); + $stmt = $pdo->prepare("SELECT COUNT(id) FROM users WHERE status = 'active'"); $stmt->execute(); return (int) $stmt->fetchColumn(); } @@ -25,24 +18,20 @@ public static function countAll(): int public static function getRecent(int $limit): array { $pdo = Database::getConnection(); - $query = "SELECT id, name, email, role, status, created_at - FROM users - WHERE status = 'active' - ORDER BY created_at DESC - LIMIT :limit"; - $stmt = $pdo->prepare($query); + $stmt = $pdo->prepare("SELECT id, name, email, role, status, created_at + FROM users + WHERE status = 'active' + ORDER BY created_at DESC + LIMIT :limit"); $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC); } - // --- MÉTODOS DE CRUD --- - public static function getAll(): array { $pdo = Database::getConnection(); - $query = "SELECT * FROM users ORDER BY created_at DESC"; - $stmt = $pdo->prepare($query); + $stmt = $pdo->prepare("SELECT * FROM users ORDER BY created_at DESC"); $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC); } @@ -50,8 +39,7 @@ public static function getAll(): array public static function findById(int $id) { $pdo = Database::getConnection(); - $query = "SELECT * FROM users WHERE id = :id"; - $stmt = $pdo->prepare($query); + $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); $stmt->bindParam(':id', $id); $stmt->execute(); $user = $stmt->fetch(PDO::FETCH_ASSOC); @@ -65,35 +53,32 @@ public static function findById(int $id) public static function findByEmail(string $email) { $pdo = Database::getConnection(); - $query = "SELECT * FROM users WHERE email = :email AND status = 'active'"; - $stmt = $pdo->prepare($query); + $stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email AND status = 'active'"); $stmt->bindParam(':email', $email); $stmt->execute(); return $stmt->fetch(PDO::FETCH_ASSOC); } - public static function create(array $data) + public static function create(array $data): bool { $pdo = Database::getConnection(); $data['status'] = 'active'; $data['force_password_change'] = 1; - $query = "INSERT INTO users (name, email, birthdate, password_hash, role, status, force_password_change) - VALUES (:name, :email, :birthdate, :password_hash, :role, :status, :force_password_change)"; + $stmt = $pdo->prepare("INSERT INTO users (name, email, birthdate, password, role, status, force_password_change) + VALUES (:name, :email, :birthdate, :password, :role, :status, :force_password_change)"); - $stmt = $pdo->prepare($query); return $stmt->execute($data); } public static function update(int $id, array $data): bool { if (isset($data['cnpj'])) { - $encryptedCnpj = self::encryptCnpj($data['cnpj']); - if ($encryptedCnpj === false) { - // Se a encriptação falhar, não continue para não salvar um valor inválido + $encrypted = self::encryptCnpj($data['cnpj']); + if ($encrypted === false) { return false; } - $data['cnpj'] = $encryptedCnpj; + $data['cnpj'] = $encrypted; } if (empty($data)) { @@ -105,9 +90,8 @@ public static function update(int $id, array $data): bool foreach (array_keys($data) as $key) { $fields[] = "$key = :$key"; } - $query = "UPDATE users SET " . implode(', ', $fields) . " WHERE id = :id"; - $stmt = $pdo->prepare($query); + $stmt = $pdo->prepare("UPDATE users SET " . implode(', ', $fields) . " WHERE id = :id"); $data['id'] = $id; return $stmt->execute($data); @@ -118,51 +102,41 @@ public static function delete(int $id): bool return self::update($id, ['status' => 'inactive']); } - // --- MÉTODOS DE CNPJ --- - public static function isCnpjInUse(string $cnpj, int $excludeUserId): bool { $pdo = Database::getConnection(); - $query = "SELECT id, cnpj FROM users WHERE id != :exclude_id AND cnpj IS NOT NULL"; - $stmt = $pdo->prepare($query); + $stmt = $pdo->prepare("SELECT id, cnpj FROM users WHERE id != :exclude_id AND cnpj IS NOT NULL"); $stmt->bindParam(':exclude_id', $excludeUserId); $stmt->execute(); $users = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($users as $user) { - $decryptedCnpj = self::decryptCnpj($user['cnpj']); - if ($decryptedCnpj === $cnpj) { + if (self::decryptCnpj($user['cnpj']) === $cnpj) { return true; } } return false; } - // --- MÉTODOS DE SENHA --- - public static function clearPasswordChangeFlag(int $userId): bool { return self::update($userId, ['force_password_change' => 0]); } - public static function updatePassword($email, $newPasswordHash) + public static function updatePassword(string $email, string $newPasswordHash): bool { $pdo = Database::getConnection(); - try { - $stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE email = ?"); + $stmt = $pdo->prepare("UPDATE users SET password = ? WHERE email = ?"); return $stmt->execute([$newPasswordHash, $email]); } catch (\Exception $e) { return false; } } - // --- MÉTODOS DE CRIPTOGRAFIA --- - private static function getIvBinary() { if (!defined('ENCRYPTION_IV') || strlen(ENCRYPTION_IV) !== 32 || !ctype_xdigit(ENCRYPTION_IV)) { - // Lançar um erro ou logar, pois o IV não é um hexadecimal de 32 caracteres. error_log("ENCRYPTION_IV não está definida ou não é um hexadecimal de 32 caracteres."); return false; } @@ -178,13 +152,13 @@ private static function encryptCnpj(string $cnpj) return openssl_encrypt($cnpj, 'aes-256-cbc', ENCRYPTION_KEY, 0, $iv); } - private static function decryptCnpj(?string $encrypted_cnpj) + private static function decryptCnpj(?string $encryptedCnpj) { $iv = self::getIvBinary(); - if ($iv === false || empty($encrypted_cnpj || !extension_loaded('openssl') || !defined('ENCRYPTION_KEY'))) { + if ($iv === false || empty($encryptedCnpj) || !extension_loaded('openssl') || !defined('ENCRYPTION_KEY')) { return null; } - $decrypted = openssl_decrypt($encrypted_cnpj, 'aes-256-cbc', ENCRYPTION_KEY, 0, $iv); + $decrypted = openssl_decrypt($encryptedCnpj, 'aes-256-cbc', ENCRYPTION_KEY, 0, $iv); return $decrypted === false ? null : $decrypted; } -} +} \ No newline at end of file diff --git a/app/Models/Venue.php b/app/Models/Venue.php index 71d9b19..8f69413 100644 --- a/app/Models/Venue.php +++ b/app/Models/Venue.php @@ -28,34 +28,73 @@ public static function countAll(): int * Busca todas as quadras disponíveis (para utilizadores finais). * @return array */ - public static function getAll(): array + public static function getAll() { - $pdo = Database::getConnection(); - $query = "SELECT v.*, a.street, a.number, a.city - FROM venues v - JOIN addresses a ON v.address_id = a.id - WHERE v.status = 'available'"; - $stmt = $pdo->prepare($query); - $stmt->execute(); - return $stmt->fetchAll(PDO::FETCH_ASSOC); + try { + $conn = \App\Core\Database::getConnection(); + + // Fazemos os JOINs encadeados E buscamos a imagem de capa! + $sql = "SELECT v.*, + s.name AS street, + a.number, + a.cep, + a.complement, + n.name AS neighborhood, + c.name AS city, + st.name AS state, + (SELECT file_path FROM venues_images vi WHERE vi.venue_id = v.id ORDER BY id ASC LIMIT 1) AS image_path + FROM venues v + JOIN addresses a ON v.address_id = a.id + JOIN streets s ON a.street_id = s.id + JOIN neighborhoods n ON s.neighborhood_id = n.id + JOIN cities c ON n.city_id = c.id + JOIN states st ON c.state_id = st.id"; + + $stmt = $conn->query($sql); + + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + + } catch (\PDOException $e) { + error_log("Erro ao buscar todas as quadras para o mapa: " . $e->getMessage()); + return []; // Retorna array vazio para não quebrar a tela do mapa + } } /** * Busca TODAS as quadras no sistema para o painel de admin. * @return array */ - public static function getAllForAdmin(): array + public static function getAllForAdmin() { - $pdo = Database::getConnection(); - $query = "SELECT v.*, u.name as owner_name, a.street, a.number - FROM venues v - LEFT JOIN users u ON v.user_id = u.id - LEFT JOIN addresses a ON v.address_id = a.id - ORDER BY v.created_at DESC"; - - $stmt = $pdo->prepare($query); - $stmt->execute(); - return $stmt->fetchAll(PDO::FETCH_ASSOC); + try { + $conn = \App\Core\Database::getConnection(); + + // Fazemos os JOINs encadeados para as tabelas de endereço e buscamos a capa + $sql = "SELECT v.*, + s.name AS street, + a.number, + a.cep, + a.complement, + n.name AS neighborhood, + c.name AS city, + st.name AS state, + (SELECT file_path FROM venues_images vi WHERE vi.venue_id = v.id ORDER BY id ASC LIMIT 1) AS image_path + FROM venues v + JOIN addresses a ON v.address_id = a.id + JOIN streets s ON a.street_id = s.id + JOIN neighborhoods n ON s.neighborhood_id = n.id + JOIN cities c ON n.city_id = c.id + JOIN states st ON c.state_id = st.id + ORDER BY v.created_at DESC"; + + $stmt = $conn->query($sql); + + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + + } catch (\PDOException $e) { + error_log("Erro ao buscar todas as quadras para o admin: " . $e->getMessage()); + return []; // Retorna um array vazio para não quebrar a tela de listagem + } } /** @@ -63,19 +102,32 @@ public static function getAllForAdmin(): array * @param int $userId * @return array */ - public static function findByUserId(int $userId): array + public static function findByUserId($userId) { - $pdo = Database::getConnection(); - $query = "SELECT v.*, a.street, a.number, - (SELECT vi.file_path FROM venue_images vi WHERE vi.venue_id = v.id ORDER BY vi.id DESC LIMIT 1) as image_path - FROM venues v - LEFT JOIN addresses a ON v.address_id = a.id - WHERE v.user_id = :user_id AND v.status = 'available' - ORDER BY v.created_at DESC"; - $stmt = $pdo->prepare($query); - $stmt->bindParam(':user_id', $userId, PDO::PARAM_INT); - $stmt->execute(); - return $stmt->fetchAll(PDO::FETCH_ASSOC); + try { + $conn = \App\Core\Database::getConnection(); + + // Adicionamos uma subquery (SELECT file_path...) para buscar a capa da quadra + $sql = "SELECT v.*, + s.name AS street, + a.number, + a.cep, + a.complement, + (SELECT file_path FROM venues_images vi WHERE vi.venue_id = v.id ORDER BY id ASC LIMIT 1) AS image_path + FROM venues v + JOIN addresses a ON v.address_id = a.id + JOIN streets s ON a.street_id = s.id + WHERE v.user_id = ?"; + + $stmt = $conn->prepare($sql); + $stmt->execute([$userId]); + + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + + } catch (\PDOException $e) { + error_log("Erro ao buscar as quadras do usuário: " . $e->getMessage()); + return []; // Retorna array vazio em caso de erro para não quebrar a tela + } } /** @@ -100,18 +152,39 @@ public static function findById(int $id) * Busca todas as quadras com coordenadas para exibição no mapa. * @return array */ - public static function getAllWithCoordinates(): array + public static function getAllWithCoordinates() { - $pdo = Database::getConnection(); - $query = "SELECT v.id, v.name, a.street, a.number, a.city, a.latitude, a.longitude, - u.name as owner_name, - (SELECT vi.file_path FROM venue_images vi WHERE vi.venue_id = v.id ORDER BY vi.id DESC LIMIT 1) as image_path - FROM venues v - JOIN addresses a ON v.address_id = a.id - LEFT JOIN users u ON v.user_id = u.id WHERE v.status = 'available'"; - $stmt = $pdo->prepare($query); - $stmt->execute(); - return $stmt->fetchAll(PDO::FETCH_ASSOC); + try { + $conn = \App\Core\Database::getConnection(); + + // Fazemos os JOINs encadeados, buscamos as coordenadas reais e a imagem de capa + $sql = "SELECT v.*, + s.name AS street, + a.number, + a.cep, + a.complement, + a.latitude, + a.longitude, + n.name AS neighborhood, + c.name AS city, + st.name AS state, + (SELECT file_path FROM venues_images vi WHERE vi.venue_id = v.id ORDER BY id ASC LIMIT 1) AS image_path + FROM venues v + JOIN addresses a ON v.address_id = a.id + JOIN streets s ON a.street_id = s.id + JOIN neighborhoods n ON s.neighborhood_id = n.id + JOIN cities c ON n.city_id = c.id + JOIN states st ON c.state_id = st.id + WHERE a.latitude IS NOT NULL AND a.longitude IS NOT NULL"; + + $stmt = $conn->query($sql); + + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + + } catch (\PDOException $e) { + error_log("Erro ao buscar quadras com coordenadas para o mapa: " . $e->getMessage()); + return []; // Retorna array vazio para não quebrar a tela + } } /** diff --git a/app/Models/VenueImage.php b/app/Models/VenueImage.php index bf14717..e46a501 100644 --- a/app/Models/VenueImage.php +++ b/app/Models/VenueImage.php @@ -3,7 +3,6 @@ namespace App\Models; use App\Core\Database; - use PDO; /** @@ -21,7 +20,8 @@ class VenueImage public static function create(int $venueId, string $filePath): bool { $pdo = Database::getConnection(); - $sql = "INSERT INTO venue_images (venue_id, file_path) VALUES (:venue_id, :file_path)"; + // CORRIGIDO: venue_images para venues_images + $sql = "INSERT INTO venues_images (venue_id, file_path) VALUES (:venue_id, :file_path)"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':venue_id', $venueId, PDO::PARAM_INT); $stmt->bindParam(':file_path', $filePath); @@ -36,7 +36,8 @@ public static function create(int $venueId, string $filePath): bool public static function findByVenueId(int $venueId): array { $pdo = Database::getConnection(); - $sql = "SELECT * FROM venue_images WHERE venue_id = :venue_id ORDER BY id DESC"; + // CORRIGIDO: venue_images para venues_images + $sql = "SELECT * FROM venues_images WHERE venue_id = :venue_id ORDER BY id DESC"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':venue_id', $venueId, PDO::PARAM_INT); $stmt->execute(); @@ -46,12 +47,14 @@ public static function findByVenueId(int $venueId): array public static function findById(int $id) { $pdo = Database::getConnection(); - $sql = "SELECT * FROM venue_images WHERE id = :id"; + // CORRIGIDO: venue_images para venues_images + $sql = "SELECT * FROM venues_images WHERE id = :id"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':id', $id, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetch(PDO::FETCH_ASSOC); } + /** * Deleta uma imagem específica pelo seu ID. * @param int $imageId O ID da imagem a ser deletada. @@ -60,9 +63,10 @@ public static function findById(int $id) public static function delete(int $imageId): bool { $pdo = Database::getConnection(); - $sql = "DELETE FROM venue_images WHERE id = :id"; + // CORRIGIDO: venue_images para venues_images + $sql = "DELETE FROM venues_images WHERE id = :id"; $stmt = $pdo->prepare($sql); $stmt->bindParam(':id', $imageId, PDO::PARAM_INT); return $stmt->execute(); } -} +} \ No newline at end of file diff --git a/app/views/admin/dashboard.php b/app/views/admin/dashboard.php index 878e58c..5c11309 100644 --- a/app/views/admin/dashboard.php +++ b/app/views/admin/dashboard.php @@ -13,7 +13,7 @@ - Kolae - Admin Dashboard + Kolae diff --git a/app/views/admin/map.php b/app/views/admin/map.php index 0cf9344..f4f8d96 100644 --- a/app/views/admin/map.php +++ b/app/views/admin/map.php @@ -4,7 +4,7 @@ - Kolae - Mapa de Quadras + Kolae diff --git a/app/views/admin/users.php b/app/views/admin/users.php index 273fbb0..2193db0 100644 --- a/app/views/admin/users.php +++ b/app/views/admin/users.php @@ -4,7 +4,7 @@ - Kolae - Gerir Usuários + Kolae diff --git a/app/views/home/index.php b/app/views/home/index.php index 470b8fd..8c83fb8 100644 --- a/app/views/home/index.php +++ b/app/views/home/index.php @@ -80,143 +80,174 @@ -
-
- - Logo Kolae +
+
+ + + + Logo Kolae + + + + + +
+ + + -
-
- + +
+
+ + + + -
-
+
+
@@ -239,94 +270,189 @@ class="w-8 h-8 rounded-full object-cover border border-gray-600"
-
-
-

- -

-

- -

-
-
- +
+
+
+ + +
+ +
+ + + Ilustração da equipe Kolae +
+ + +
+

+

+ + +
+ + +
+ +
+ +
+

+

+
+ + +
+ +
+ +
+

+

+
+
+
+ +
+
-
-

-

+
+

+

+
+ + +
+ + +
+ + +
+ Logo Fatec +
+ +
+ Logo Adidas +
+ +
+ Logo Nike +
+ +
+ Logo Atletica Sagui +
+ +
+ Logo BRTZ +
+ +
+ Logo Leos de Ferraz
-
-
-
Logo Fatec
-
Logo Adidas
-
Logo Nike
-
Logo Atletica Sagui
-
Logo BRTZ
-
Logo Leos de Ferraz
-
Logo Fatec
-
Logo Adidas
-
Logo Nike
-
Logo Atletica Sagui
-
Logo BRTZ
-
Logo Leos de Ferraz
-
+ + + + + +
+ Logo Fatec
-
+ +
+ Logo Adidas +
+ +
+ Logo Nike +
+ +
+ Logo Atletica Sagui +
+ +
+ Logo BRTZ +
+ +
+ Logo Leos de Ferraz +
+ + + +
-