refactor for the new tree and add todos

This commit is contained in:
glopes 2026-02-08 17:41:26 -03:00
parent 016b6516b2
commit 44adffb713
14 changed files with 173 additions and 173 deletions

View File

@ -5,10 +5,10 @@ const Database = require("better-sqlite3");
const path = require("path"); const path = require("path");
const crypto = require("crypto"); const crypto = require("crypto");
const DB_PATH = path.resolve(__dirname, "../data/data.db");
function callDb(fn) { function callDb(fn) {
const db = new Database(path.resolve(__dirname, "../data/auth.db"), { const db = new Database(DB_PATH, { fileMustExist: true });
fileMustExist: true,
});
try { try {
fn(db); fn(db);
} finally { } finally {
@ -20,118 +20,120 @@ function gen(bytes) {
return crypto.randomBytes(bytes).toString("hex"); return crypto.randomBytes(bytes).toString("hex");
} }
/**
* adduser <user_name> <source>
*/
function addUser(args) { function addUser(args) {
const [username, email, status, displayName] = args; const [userName, source] = args;
if (!userName || !source) {
console.error("usage: adduser <user_name> <source>");
process.exit(1);
}
callDb((db) => { callDb((db) => {
const res = db const res = db
.prepare( .prepare(
` `
INSERT INTO users (username, email, status, display_name) INSERT INTO users (
VALUES (?, ?, ?, ?) user_name,
user_source,
user_status,
user_created_at
)
VALUES (?, ?, 1, ?)
`, `,
) )
.run(username, email, status, displayName); .run(userName, source, Date.now());
console.log("user_id:", res.lastInsertRowid); console.log("user_id:", res.lastInsertRowid);
}); });
} }
function disUser(args) { /**
const [username] = args; * genkey <user_name>
*/
callDb((db) => { function genKey(args) {
db.prepare( const [userName] = args;
`
UPDATE users SET status = 'disabled'
WHERE username = ?
`,
).run(username);
console.log("user disabled:", username);
});
}
function listUser(args) {
const [username] = args;
callDb((db) => { callDb((db) => {
const user = db const user = db
.prepare( .prepare(`SELECT user_id FROM users WHERE user_name = ?`)
` .get(userName);
SELECT user_id, username, email, status, display_name, created_at
FROM users
WHERE username = ?
`,
)
.get(username);
if (!user) { if (!user) {
console.log("user not found"); console.error("user not found");
return; process.exit(1);
} }
console.log("USER"); const apiKey = gen(24); // 48 hex chars
console.log(user); const apiSecret = gen(48); // 96 hex chars
const keys = db
.prepare(
`
SELECT api_key, status, created_at, last_used_at
FROM api_keys
WHERE user_id = ?
`,
)
.all(user.user_id);
if (keys.length) {
console.log("\nAPI_KEYS");
keys.forEach((k) => console.log(k));
}
});
}
function addKey(args) {
const [username] = args;
callDb((db) => {
const user = db
.prepare(
`
SELECT user_id FROM users WHERE username = ?
`,
)
.get(username);
const apiKey = gen(24);
const apiSecret = gen(48);
db.prepare( db.prepare(
` `
INSERT INTO api_keys (user_id, api_key, api_secret, status) UPDATE users
VALUES (?, ?, ?, 'active') SET user_api_key = ?, user_api_secret = ?
WHERE user_id = ?
`, `,
).run(user.user_id, apiKey, apiSecret); ).run(apiKey, apiSecret, user.user_id);
console.log("api_key:", apiKey); console.log("api_key:", apiKey);
console.log("api_secret:", apiSecret); console.log("api_secret:", apiSecret);
}); });
} }
function disKey(args) { /**
const [apiKey] = args; * disable <user_name>
*/
function disableUser(args) {
const [userName] = args;
callDb((db) => { callDb((db) => {
db.prepare( db.prepare(
` `
UPDATE api_keys SET status = 'revoked' UPDATE users SET user_status = 0
WHERE api_key = ? WHERE user_name = ?
`, `,
).run(apiKey); ).run(userName);
console.log("api_key revoked"); console.log("user disabled:", userName);
}); });
} }
/**
* list <user_name>
*/
function listUser(args) {
const [userName] = args;
callDb((db) => {
const user = db
.prepare(
`
SELECT
user_id,
user_name,
user_source,
user_status,
user_api_key,
user_created_at
FROM users
WHERE user_name = ?
`,
)
.get(userName);
if (!user) {
console.log("user not found");
return;
}
console.log(user);
});
}
/* ======================== */
const argv = process.argv.slice(2); const argv = process.argv.slice(2);
const cmd = argv[0]; const cmd = argv[0];
const args = argv.slice(1); const args = argv.slice(1);
@ -141,23 +143,20 @@ switch (cmd) {
addUser(args); addUser(args);
break; break;
case "disuser": case "genkey":
disUser(args); genKey(args);
break; break;
case "listuser": case "disable":
disableUser(args);
break;
case "list":
listUser(args); listUser(args);
break; break;
case "addkey":
addKey(args);
break;
case "diskey":
disKey(args);
break;
default: default:
console.error("Unknown command"); console.error("Unknown command");
console.error("Commands: adduser | genkey | disable | list");
process.exit(1); process.exit(1);
} }

View File

@ -4,9 +4,9 @@ declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';
use FrameworkX\App; use FrameworkX\App;
use Bass\Webclient\Auth\Middleware\HmacAuthMiddleware; use Bass\Webclient\Middleware\HmacAuthMiddleware;
use Bass\Webclient\Controllers\RequestController; use Bass\Webclient\Controllers\RequestController;
use Bass\Webclient\Http\ResponseLib; use Bass\Webclient\Libs\ResponseLib;
ini_set('display_errors', '1'); ini_set('display_errors', '1');
ini_set('display_startup_errors', '1'); ini_set('display_startup_errors', '1');
@ -16,14 +16,12 @@ $app = new App();
$hmacAuth = new HmacAuthMiddleware(); $hmacAuth = new HmacAuthMiddleware();
$app->post( $app->post(
'/v1/request', '/v1/request',
$hmacAuth, $hmacAuth,
RequestController::class RequestController::class
); );
$app->get( $app->get(
'/health', '/health',
fn() => ResponseLib::sendOk(['status' => 'ok']) fn() => ResponseLib::sendOk(['status' => 'ok'])

View File

@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Bass\Webclient\Controllers; namespace Bass\Webclient\Controllers;
use Bass\Webclient\Libs\RequestLib; use Bass\Webclient\Libs\RequestLib;
use Bass\Webclient\Http\ResponseLib;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response; use React\Http\Message\Response;
class RequestController class RequestController
{ {
public function __invoke(ServerRequestInterface $request): Response public function __invoke(ServerRequestInterface $request): Response

View File

@ -2,11 +2,9 @@
declare(strict_types=1); declare(strict_types=1);
namespace Bass\Webclient\Libs; namespace Bass\Webclient\Libs;
use Bass\Webclient\Model\LeadModel;
use Bass\Webclient\Domain\Client\ClientModel; use Bass\Webclient\Schema\LeadSchema;
use React\Http\Message\Response; use React\Http\Message\Response;
use Bass\Webclient\Http\ResponseLib;
use Bass\Webclient\Schema\ClientCreateSchema;
class RequestLib class RequestLib
{ {
@ -36,71 +34,51 @@ class RequestLib
); );
} }
$schema = ClientCreateSchema::schema(); $schema = LeadSchema::schema();
[$ok, $err] = GuardLib::requireJsonObject($data); [$ok, $err] = GuardLib::requireJsonObject($data);
if (!$ok) { if (!$ok) {
return ResponseLib::sendFail( return ResponseLib::sendFail($err['message'], 400, $err);
$err['message'],
400,
$err
);
} }
[$ok, $err] = GuardLib::maxPayloadFields($data); [$ok, $err] = GuardLib::maxPayloadFields($data);
if (!$ok) { if (!$ok) {
return ResponseLib::sendFail( return ResponseLib::sendFail($err['message'], 400, $err);
$err['message'],
400,
$err
);
} }
[$ok, $err] = GuardLib::allowOnlyFields($data, array_keys($schema)); [$ok, $err] = GuardLib::allowOnlyFields($data, array_keys($schema));
if (!$ok) { if (!$ok) {
return ResponseLib::sendFail( return ResponseLib::sendFail($err['message'], 400, $err);
$err['message'],
400,
$err
);
} }
[$ok, $err] = GuardLib::blockDangerousPatterns($data); [$ok, $err] = GuardLib::blockDangerousPatterns($data);
if (!$ok) { if (!$ok) {
return ResponseLib::sendFail( return ResponseLib::sendFail($err['message'], 400, $err);
$err['message'],
400,
$err
);
} }
[$ok, $err] = GuardLib::requiredBySchema($data, $schema); [$ok, $err] = GuardLib::requiredBySchema($data, $schema);
if (!$ok) { if (!$ok) {
return ResponseLib::sendFail( return ResponseLib::sendFail($err['message'], 422, $err);
$err['message'],
422,
$err
);
} }
[$ok, $err] = GuardLib::validateBySchema($data, $schema); [$ok, $err] = GuardLib::validateBySchema($data, $schema);
if (!$ok) { if (!$ok) {
return ResponseLib::sendFail( return ResponseLib::sendFail($err['message'], 422, $err);
$err['message'],
422,
$err
);
} }
[, $data] = SanitizationLib::cleanBySchema($data, $schema); [, $data] = SanitizationLib::cleanBySchema($data, $schema);
[$ok, $result] = (new ClientModel())->insert($data);
[$ok, $result] = (new LeadModel())->insert($data);
if (!$ok) { if (!$ok) {
return ResponseLib::sendFail( return ResponseLib::sendFail(
'Internal server error', 'Internal server error',
500, 500,
[ ['code' => 'DATABASE_ERROR']
'code' => 'DATABASE_ERROR'
]
); );
} }
self::sendEmails($data); self::sendEmails($data);
return ResponseLib::sendOk( return ResponseLib::sendOk(
[ [
'lead_id' => $result['lead_id'], 'lead_id' => $result['lead_id'],

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Bass\Webclient\Http; namespace Bass\Webclient\libs;
use React\Http\Message\Response; use React\Http\Message\Response;

View File

@ -1,10 +1,10 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Bass\Webclient\Auth\Middleware; namespace Bass\Webclient\Middleware;
use Bass\Webclient\Http\ResponseLib; use Bass\Webclient\Libs\ResponseLib;
use Bass\Webclient\Auth\Models\ApiKeyModel; use Bass\Webclient\Model\ApiKeyModel;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response; use React\Http\Message\Response;
@ -29,7 +29,7 @@ class HmacAuthMiddleware
); );
} }
// 🔒 FORMATO DA API KEY (fail fast) // 🔒 API key format (fail fast)
if (!preg_match(self::API_KEY_REGEX, $apiKey)) { if (!preg_match(self::API_KEY_REGEX, $apiKey)) {
return ResponseLib::sendFail( return ResponseLib::sendFail(
'Invalid API key format', 'Invalid API key format',
@ -38,6 +38,7 @@ class HmacAuthMiddleware
); );
} }
// ⏱ replay protection (5 min window)
if (abs(time() - (int) $timestamp) > 300) { if (abs(time() - (int) $timestamp) > 300) {
return ResponseLib::sendFail( return ResponseLib::sendFail(
'Expired request', 'Expired request',
@ -66,8 +67,6 @@ class HmacAuthMiddleware
); );
} }
(new ApiKeyModel())->touchLastUsed($apiKey);
return $next( return $next(
$request->withAttribute('auth', [ $request->withAttribute('auth', [
'user_id' => $result['user_id'], 'user_id' => $result['user_id'],

View File

@ -1,11 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Bass\Webclient\Auth\Middleware; namespace Bass\Webclient\Middleware;
use Bass\Webclient\Libs\ResponseLib;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response; use React\Http\Message\Response;
use Bass\Webclient\Http\ResponseLib;
class RateLimitMiddleware class RateLimitMiddleware
{ {
@ -19,8 +19,10 @@ class RateLimitMiddleware
$this->windowSeconds = $windowSeconds; $this->windowSeconds = $windowSeconds;
} }
public function __invoke(ServerRequestInterface $request, callable $next): Response public function __invoke(
{ ServerRequestInterface $request,
callable $next
): Response {
$apiKey = $request->getHeaderLine('X-API-KEY'); $apiKey = $request->getHeaderLine('X-API-KEY');
if (!$apiKey) { if (!$apiKey) {
@ -31,7 +33,7 @@ class RateLimitMiddleware
); );
} }
// 🔒 rate-limit SOMENTE para chave válida // 🔒 rate-limit por API key (em memória)
if (!$this->allowRequest($apiKey)) { if (!$this->allowRequest($apiKey)) {
return ResponseLib::sendFail( return ResponseLib::sendFail(
'Rate limit exceeded', 'Rate limit exceeded',
@ -47,7 +49,8 @@ class RateLimitMiddleware
} }
/** /**
* Controle de rate-limit em memória * In-memory rate-limit control
* (per process, per API key)
*/ */
private function allowRequest(string $key): bool private function allowRequest(string $key): bool
{ {

View File

@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Bass\Webclient\Auth\Models; namespace Bass\Webclient\Model;
use Bass\Webclient\Infra\ModelFactory; use Bass\Webclient\Model\ModelFactory;
class ApiKeyModel class ApiKeyModel
{ {
@ -11,11 +11,11 @@ class ApiKeyModel
{ {
$sql = " $sql = "
SELECT SELECT
user_api_key as api_key, user_api_key AS api_key,
user_api_secret as api_secret, user_api_secret AS api_secret,
user_status as status, user_status AS status,
user_id, user_id,
user_name as username user_name AS username
FROM users FROM users
WHERE user_api_key = :api_key WHERE user_api_key = :api_key
AND user_status = 1 AND user_status = 1

View File

@ -1,12 +1,13 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Bass\Webclient\Domain\Client; namespace Bass\Webclient\Model;
use Bass\Webclient\Infra\ModelFactory;
use PDO; use PDO;
use Bass\Webclient\Model\ModelFactory;
// TODO: ADD TAXID TO BE LIMIT ONE REQUEST PER VALID USER ID
class ClientModel class LeadModel
{ {
private PDO $db; private PDO $db;
@ -19,13 +20,33 @@ class ClientModel
{ {
$sql = " $sql = "
INSERT INTO lead INSERT INTO lead
(lead_name, lead_phone, lead_email, lead_company, lead_sector, lead_employees, lead_revenue, lead_description) (
lead_name,
lead_phone,
lead_email,
lead_company,
lead_sector,
lead_employees,
lead_revenue,
lead_description
)
VALUES VALUES
(:name, :phone, :email, :company_name, :sector, :number_of_employees, :revenue, :description) (
:name,
:phone,
:email,
:company_name,
:sector,
:number_of_employees,
:revenue,
:description
)
"; ";
try { try {
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute($data); $stmt->execute($data);
return [ return [
true, true,
[ [
@ -37,7 +58,7 @@ class ClientModel
false, false,
[ [
'code' => 'DB_ERROR', 'code' => 'DB_ERROR',
'message' => 'Failed to insert client' 'message' => 'Failed to insert lead'
] ]
]; ];
} }

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Bass\Webclient\Infra; namespace Bass\Webclient\Model;
use PDO; use PDO;

View File

@ -33,7 +33,7 @@ class LeadSchema
'max' => 60, 'max' => 60,
], ],
'number_of_employees' => [ 'number_of_employees' => [
'required' => false, 'required' => true,
'type' => 'int', 'type' => 'int',
'min' => 1, 'min' => 1,
], ],

View File

@ -0,0 +1 @@
<!-- TODO: add a validation for cnpj or cpf -->

View File

@ -1,5 +1,6 @@
PRAGMA foreign_keys=ON; PRAGMA foreign_keys=ON;
-- TODO: ADD TAXID TO BE LIMIT ONE REQUEST PER VALID USER ID
CREATE TABLE CREATE TABLE
users ( users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER PRIMARY KEY AUTOINCREMENT,

View File

@ -4,9 +4,9 @@ set -euo pipefail
# ========= CONFIG ========= # ========= CONFIG =========
URL="http://127.0.0.1:8080/v1/request" URL="http://127.0.0.1:8080/v1/request"
API_KEY="test_api_key" API_KEY="4677fb5d0258618550bdceacaf0acf39e5a38de18855bb65"
API_SECRET="test_api_secret" API_SECRET="5177347e4209991841d6721d733c831caf3752424941a5c9d3560dccb5e40711df34f9df7a9dda667a26be21a7e699ac"
API_USER="test_user" API_USER="my-client"
# ========= PAYLOAD ========= # ========= PAYLOAD =========
BODY='{ BODY='{