forked from BassPago/leeds_backend
refactor tree
This commit is contained in:
parent
b058ce933f
commit
016b6516b2
31
readme.md
Normal file
31
readme.md
Normal 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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
51
src/Model/ApiKeyModel.php
Normal 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'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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;');
|
||||
|
|
@ -3,7 +3,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Bass\Webclient\Schema;
|
||||
|
||||
class ClientCreateSchema
|
||||
class LeadSchema
|
||||
{
|
||||
public static function schema(): array
|
||||
{
|
||||
Loading…
Reference in New Issue
Block a user