refactor tree

This commit is contained in:
glopes 2026-02-06 21:20:48 -03:00
parent b058ce933f
commit 016b6516b2
11 changed files with 94 additions and 124 deletions

31
readme.md Normal file
View File

@ -0,0 +1,31 @@
├── bin
│   ├── email-cli
│   └── support-cli
├── data
│   └── data.db
├── logs
├── public
│   └── index.php
├── readme.md
├── src
│   ├── Middleware
│   ├ |── HmacAuthMiddleware.php
│   │   └── RateLImitMiddleware.php
│   ├── Controllers
│   │   └── RequestController.php
│   ├── Model
│   │   └── ModelFactory.php
│   │   └── LeadModel.php
│   ├── Libs
│   │   └── ResponseLib.php
│   │   ├── ExecLib.php
│   │   ├── GuardLib.php
│   │   ├── RequestLib.php
│   │   └── SanitizationLib.php
│   ├── migration
│   │   └── schema.sql
│   └── Schema
│   └── LeadSchema.php
└── test
├── hmac_curl.sh
└── request.sh

View File

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace Bass\Webclient\Auth\Infra;
use PDO;
class AuthModelFactory
{
private static ?PDO $db = null;
public static function db(): PDO
{
if (self::$db === null) {
self::$db = new PDO('sqlite:' . __DIR__ . '/../../../data/auth.db');
self::$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
self::$db->exec('PRAGMA foreign_keys = ON;');
}
return self::$db;
}
}

View File

@ -1,65 +0,0 @@
<?php
declare(strict_types=1);
namespace Bass\Webclient\Auth\Models;
use Bass\Webclient\Auth\Infra\AuthModelFactory;
class ApiKeyModel
{
public function findActiveByKey(string $apiKey): array
{
$sql = "
SELECT
ak.api_key,
ak.api_secret,
ak.status,
u.user_id,
u.username
FROM api_keys ak
JOIN users u ON u.user_id = ak.user_id
WHERE ak.api_key = :api_key
AND ak.status = 'active'
AND u.status = 'active'
LIMIT 1
";
try {
$stmt = AuthModelFactory::db()->prepare($sql);
$stmt->execute(['api_key' => $apiKey]);
$row = $stmt->fetch();
if (!$row) {
return [
false,
[
'code' => 'API_KEY_NOT_FOUND',
'message' => 'Invalid API key'
]
];
}
return [true, $row];
} catch (\Throwable $e) {
return [
false,
[
'code' => 'AUTH_DB_ERROR',
'message' => 'Failed to query auth database'
]
];
}
}
public function touchLastUsed(string $apiKey): void
{
$sql = "UPDATE api_keys SET last_used_at = datetime('now') WHERE api_key = :api_key";
try {
AuthModelFactory::db()
->prepare($sql)
->execute(['api_key' => $apiKey]);
} catch (\Throwable $e) {
// falha aqui NÃO bloqueia request
}
}
}

View File

@ -103,7 +103,7 @@ class RequestLib
self::sendEmails($data);
return ResponseLib::sendOk(
[
'client_id' => $result['client_id'],
'lead_id' => $result['lead_id'],
'status' => 'received'
],
201

View File

@ -11,8 +11,7 @@ class RateLimitMiddleware
{
private int $maxRequests;
private int $windowSeconds;
private const API_KEY_REGEX = '/^[a-f0-9]{48}$/';
private array $limits = [];
public function __construct(int $maxRequests = 30, int $windowSeconds = 60)
{
@ -32,15 +31,6 @@ class RateLimitMiddleware
);
}
// 🔒 FORMATO INVÁLIDO → DROP IMEDIATO
if (!preg_match(self::API_KEY_REGEX, $apiKey)) {
return ResponseLib::sendFail(
'Invalid API key format',
401,
['code' => 'INVALID_API_KEY_FORMAT']
);
}
// 🔒 rate-limit SOMENTE para chave válida
if (!$this->allowRequest($apiKey)) {
return ResponseLib::sendFail(
@ -57,36 +47,21 @@ class RateLimitMiddleware
}
/**
* Controle de rate-limit por filesystem
* Controle de rate-limit em memória
*/
private function allowRequest(string $key): bool
{
$now = time();
$file = sys_get_temp_dir() . '/rl_' . sha1($key);
$data = [
'count' => 0,
'reset' => $now + $this->windowSeconds
];
if (file_exists($file)) {
$stored = json_decode(file_get_contents($file), true);
if (is_array($stored)) {
$data = $stored;
}
}
if ($now > $data['reset']) {
$data = [
if (!isset($this->limits[$key]) || $now > $this->limits[$key]['reset']) {
$this->limits[$key] = [
'count' => 0,
'reset' => $now + $this->windowSeconds
];
}
$data['count']++;
$this->limits[$key]['count']++;
file_put_contents($file, json_encode($data), LOCK_EX);
return $data['count'] <= $this->maxRequests;
return $this->limits[$key]['count'] <= $this->maxRequests;
}
}

51
src/Model/ApiKeyModel.php Normal file
View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Bass\Webclient\Auth\Models;
use Bass\Webclient\Infra\ModelFactory;
class ApiKeyModel
{
public function findActiveByKey(string $apiKey): array
{
$sql = "
SELECT
user_api_key as api_key,
user_api_secret as api_secret,
user_status as status,
user_id,
user_name as username
FROM users
WHERE user_api_key = :api_key
AND user_status = 1
LIMIT 1
";
try {
$stmt = ModelFactory::db()->prepare($sql);
$stmt->execute(['api_key' => $apiKey]);
$row = $stmt->fetch();
if (!$row) {
return [
false,
[
'code' => 'API_KEY_NOT_FOUND',
'message' => 'Invalid API key'
]
];
}
return [true, $row];
} catch (\Throwable $e) {
return [
false,
[
'code' => 'AUTH_DB_ERROR',
'message' => 'Failed to query auth database'
]
];
}
}
}

View File

@ -18,8 +18,8 @@ class ClientModel
public function insert(array $data): array
{
$sql = "
INSERT INTO client_request
(name, phone, email, company_name, sector, number_of_employees, revenue, description)
INSERT INTO lead
(lead_name, lead_phone, lead_email, lead_company, lead_sector, lead_employees, lead_revenue, lead_description)
VALUES
(:name, :phone, :email, :company_name, :sector, :number_of_employees, :revenue, :description)
";
@ -29,7 +29,7 @@ class ClientModel
return [
true,
[
'client_id' => (int) $this->db->lastInsertId()
'lead_id' => (int) $this->db->lastInsertId()
]
];
} catch (\Throwable $e) {

View File

@ -9,7 +9,7 @@ class ModelFactory
{
public static function db(): PDO
{
$db = new PDO('sqlite:' . __DIR__ . '/../../data/app.db');
$db = new PDO('sqlite:' . __DIR__ . '/../../data/data.db');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$db->exec('PRAGMA foreign_keys = ON;');

View File

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace Bass\Webclient\Schema;
class ClientCreateSchema
class LeadSchema
{
public static function schema(): array
{