refactor for the new tree and add todos
This commit is contained in:
parent
016b6516b2
commit
44adffb713
177
bin/support-cli
177
bin/support-cli
|
|
@ -5,10 +5,10 @@ const Database = require("better-sqlite3");
|
|||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const DB_PATH = path.resolve(__dirname, "../data/data.db");
|
||||
|
||||
function callDb(fn) {
|
||||
const db = new Database(path.resolve(__dirname, "../data/auth.db"), {
|
||||
fileMustExist: true,
|
||||
});
|
||||
const db = new Database(DB_PATH, { fileMustExist: true });
|
||||
try {
|
||||
fn(db);
|
||||
} finally {
|
||||
|
|
@ -20,118 +20,120 @@ function gen(bytes) {
|
|||
return crypto.randomBytes(bytes).toString("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* adduser <user_name> <source>
|
||||
*/
|
||||
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) => {
|
||||
const res = db
|
||||
.prepare(
|
||||
`
|
||||
INSERT INTO users (username, email, status, display_name)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`,
|
||||
INSERT INTO users (
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
function disUser(args) {
|
||||
const [username] = args;
|
||||
|
||||
callDb((db) => {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE users SET status = 'disabled'
|
||||
WHERE username = ?
|
||||
`,
|
||||
).run(username);
|
||||
|
||||
console.log("user disabled:", username);
|
||||
});
|
||||
}
|
||||
|
||||
function listUser(args) {
|
||||
const [username] = args;
|
||||
/**
|
||||
* genkey <user_name>
|
||||
*/
|
||||
function genKey(args) {
|
||||
const [userName] = args;
|
||||
|
||||
callDb((db) => {
|
||||
const user = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT user_id, username, email, status, display_name, created_at
|
||||
FROM users
|
||||
WHERE username = ?
|
||||
`,
|
||||
)
|
||||
.get(username);
|
||||
.prepare(`SELECT user_id FROM users WHERE user_name = ?`)
|
||||
.get(userName);
|
||||
|
||||
if (!user) {
|
||||
console.log("user not found");
|
||||
return;
|
||||
console.error("user not found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("USER");
|
||||
console.log(user);
|
||||
|
||||
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);
|
||||
const apiKey = gen(24); // 48 hex chars
|
||||
const apiSecret = gen(48); // 96 hex chars
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO api_keys (user_id, api_key, api_secret, status)
|
||||
VALUES (?, ?, ?, 'active')
|
||||
UPDATE users
|
||||
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_secret:", apiSecret);
|
||||
});
|
||||
}
|
||||
|
||||
function disKey(args) {
|
||||
const [apiKey] = args;
|
||||
/**
|
||||
* disable <user_name>
|
||||
*/
|
||||
function disableUser(args) {
|
||||
const [userName] = args;
|
||||
|
||||
callDb((db) => {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE api_keys SET status = 'revoked'
|
||||
WHERE api_key = ?
|
||||
UPDATE users SET user_status = 0
|
||||
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 cmd = argv[0];
|
||||
const args = argv.slice(1);
|
||||
|
|
@ -141,23 +143,20 @@ switch (cmd) {
|
|||
addUser(args);
|
||||
break;
|
||||
|
||||
case "disuser":
|
||||
disUser(args);
|
||||
case "genkey":
|
||||
genKey(args);
|
||||
break;
|
||||
|
||||
case "listuser":
|
||||
case "disable":
|
||||
disableUser(args);
|
||||
break;
|
||||
|
||||
case "list":
|
||||
listUser(args);
|
||||
break;
|
||||
|
||||
case "addkey":
|
||||
addKey(args);
|
||||
break;
|
||||
|
||||
case "diskey":
|
||||
disKey(args);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error("Unknown command");
|
||||
console.error("Commands: adduser | genkey | disable | list");
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use FrameworkX\App;
|
||||
use Bass\Webclient\Auth\Middleware\HmacAuthMiddleware;
|
||||
use Bass\Webclient\Middleware\HmacAuthMiddleware;
|
||||
use Bass\Webclient\Controllers\RequestController;
|
||||
use Bass\Webclient\Http\ResponseLib;
|
||||
use Bass\Webclient\Libs\ResponseLib;
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('display_startup_errors', '1');
|
||||
|
|
@ -16,14 +16,12 @@ $app = new App();
|
|||
|
||||
$hmacAuth = new HmacAuthMiddleware();
|
||||
|
||||
|
||||
$app->post(
|
||||
'/v1/request',
|
||||
$hmacAuth,
|
||||
RequestController::class
|
||||
);
|
||||
|
||||
|
||||
$app->get(
|
||||
'/health',
|
||||
fn() => ResponseLib::sendOk(['status' => 'ok'])
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||
namespace Bass\Webclient\Controllers;
|
||||
|
||||
use Bass\Webclient\Libs\RequestLib;
|
||||
use Bass\Webclient\Http\ResponseLib;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use React\Http\Message\Response;
|
||||
|
||||
class RequestController
|
||||
{
|
||||
public function __invoke(ServerRequestInterface $request): Response
|
||||
|
|
|
|||
|
|
@ -2,11 +2,9 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
namespace Bass\Webclient\Libs;
|
||||
|
||||
use Bass\Webclient\Domain\Client\ClientModel;
|
||||
use Bass\Webclient\Model\LeadModel;
|
||||
use Bass\Webclient\Schema\LeadSchema;
|
||||
use React\Http\Message\Response;
|
||||
use Bass\Webclient\Http\ResponseLib;
|
||||
use Bass\Webclient\Schema\ClientCreateSchema;
|
||||
|
||||
class RequestLib
|
||||
{
|
||||
|
|
@ -36,71 +34,51 @@ class RequestLib
|
|||
);
|
||||
}
|
||||
|
||||
$schema = ClientCreateSchema::schema();
|
||||
$schema = LeadSchema::schema();
|
||||
|
||||
[$ok, $err] = GuardLib::requireJsonObject($data);
|
||||
if (!$ok) {
|
||||
return ResponseLib::sendFail(
|
||||
$err['message'],
|
||||
400,
|
||||
$err
|
||||
);
|
||||
return ResponseLib::sendFail($err['message'], 400, $err);
|
||||
}
|
||||
|
||||
[$ok, $err] = GuardLib::maxPayloadFields($data);
|
||||
if (!$ok) {
|
||||
return ResponseLib::sendFail(
|
||||
$err['message'],
|
||||
400,
|
||||
$err
|
||||
);
|
||||
return ResponseLib::sendFail($err['message'], 400, $err);
|
||||
}
|
||||
|
||||
[$ok, $err] = GuardLib::allowOnlyFields($data, array_keys($schema));
|
||||
if (!$ok) {
|
||||
return ResponseLib::sendFail(
|
||||
$err['message'],
|
||||
400,
|
||||
$err
|
||||
);
|
||||
return ResponseLib::sendFail($err['message'], 400, $err);
|
||||
}
|
||||
|
||||
[$ok, $err] = GuardLib::blockDangerousPatterns($data);
|
||||
if (!$ok) {
|
||||
return ResponseLib::sendFail(
|
||||
$err['message'],
|
||||
400,
|
||||
$err
|
||||
);
|
||||
return ResponseLib::sendFail($err['message'], 400, $err);
|
||||
}
|
||||
|
||||
[$ok, $err] = GuardLib::requiredBySchema($data, $schema);
|
||||
if (!$ok) {
|
||||
return ResponseLib::sendFail(
|
||||
$err['message'],
|
||||
422,
|
||||
$err
|
||||
);
|
||||
return ResponseLib::sendFail($err['message'], 422, $err);
|
||||
}
|
||||
|
||||
[$ok, $err] = GuardLib::validateBySchema($data, $schema);
|
||||
if (!$ok) {
|
||||
return ResponseLib::sendFail(
|
||||
$err['message'],
|
||||
422,
|
||||
$err
|
||||
);
|
||||
return ResponseLib::sendFail($err['message'], 422, $err);
|
||||
}
|
||||
|
||||
[, $data] = SanitizationLib::cleanBySchema($data, $schema);
|
||||
[$ok, $result] = (new ClientModel())->insert($data);
|
||||
|
||||
[$ok, $result] = (new LeadModel())->insert($data);
|
||||
if (!$ok) {
|
||||
return ResponseLib::sendFail(
|
||||
'Internal server error',
|
||||
500,
|
||||
[
|
||||
'code' => 'DATABASE_ERROR'
|
||||
]
|
||||
['code' => 'DATABASE_ERROR']
|
||||
);
|
||||
}
|
||||
|
||||
self::sendEmails($data);
|
||||
|
||||
return ResponseLib::sendOk(
|
||||
[
|
||||
'lead_id' => $result['lead_id'],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bass\Webclient\Http;
|
||||
namespace Bass\Webclient\libs;
|
||||
|
||||
use React\Http\Message\Response;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bass\Webclient\Auth\Middleware;
|
||||
namespace Bass\Webclient\Middleware;
|
||||
|
||||
use Bass\Webclient\Http\ResponseLib;
|
||||
use Bass\Webclient\Auth\Models\ApiKeyModel;
|
||||
use Bass\Webclient\Libs\ResponseLib;
|
||||
use Bass\Webclient\Model\ApiKeyModel;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
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)) {
|
||||
return ResponseLib::sendFail(
|
||||
'Invalid API key format',
|
||||
|
|
@ -38,6 +38,7 @@ class HmacAuthMiddleware
|
|||
);
|
||||
}
|
||||
|
||||
// ⏱ replay protection (5 min window)
|
||||
if (abs(time() - (int) $timestamp) > 300) {
|
||||
return ResponseLib::sendFail(
|
||||
'Expired request',
|
||||
|
|
@ -66,8 +67,6 @@ class HmacAuthMiddleware
|
|||
);
|
||||
}
|
||||
|
||||
(new ApiKeyModel())->touchLastUsed($apiKey);
|
||||
|
||||
return $next(
|
||||
$request->withAttribute('auth', [
|
||||
'user_id' => $result['user_id'],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bass\Webclient\Auth\Middleware;
|
||||
namespace Bass\Webclient\Middleware;
|
||||
|
||||
use Bass\Webclient\Libs\ResponseLib;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use React\Http\Message\Response;
|
||||
use Bass\Webclient\Http\ResponseLib;
|
||||
|
||||
class RateLimitMiddleware
|
||||
{
|
||||
|
|
@ -19,8 +19,10 @@ class RateLimitMiddleware
|
|||
$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');
|
||||
|
||||
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)) {
|
||||
return ResponseLib::sendFail(
|
||||
'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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
<?php
|
||||
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
|
||||
{
|
||||
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
|
||||
";
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bass\Webclient\Domain\Client;
|
||||
namespace Bass\Webclient\Model;
|
||||
|
||||
use Bass\Webclient\Infra\ModelFactory;
|
||||
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;
|
||||
|
||||
|
|
@ -18,14 +19,34 @@ class ClientModel
|
|||
public function insert(array $data): array
|
||||
{
|
||||
$sql = "
|
||||
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)
|
||||
";
|
||||
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
|
||||
)
|
||||
";
|
||||
|
||||
try {
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($data);
|
||||
|
||||
return [
|
||||
true,
|
||||
[
|
||||
|
|
@ -37,7 +58,7 @@ class ClientModel
|
|||
false,
|
||||
[
|
||||
'code' => 'DB_ERROR',
|
||||
'message' => 'Failed to insert client'
|
||||
'message' => 'Failed to insert lead'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bass\Webclient\Infra;
|
||||
namespace Bass\Webclient\Model;
|
||||
|
||||
use PDO;
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class LeadSchema
|
|||
'max' => 60,
|
||||
],
|
||||
'number_of_employees' => [
|
||||
'required' => false,
|
||||
'required' => true,
|
||||
'type' => 'int',
|
||||
'min' => 1,
|
||||
],
|
||||
|
|
|
|||
1
src/Service/LeadService.php
Normal file
1
src/Service/LeadService.php
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!-- TODO: add a validation for cnpj or cpf -->
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
PRAGMA foreign_keys=ON;
|
||||
|
||||
-- TODO: ADD TAXID TO BE LIMIT ONE REQUEST PER VALID USER ID
|
||||
CREATE TABLE
|
||||
users (
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ set -euo pipefail
|
|||
# ========= CONFIG =========
|
||||
URL="http://127.0.0.1:8080/v1/request"
|
||||
|
||||
API_KEY="test_api_key"
|
||||
API_SECRET="test_api_secret"
|
||||
API_USER="test_user"
|
||||
API_KEY="4677fb5d0258618550bdceacaf0acf39e5a38de18855bb65"
|
||||
API_SECRET="5177347e4209991841d6721d733c831caf3752424941a5c9d3560dccb5e40711df34f9df7a9dda667a26be21a7e699ac"
|
||||
API_USER="my-client"
|
||||
|
||||
# ========= PAYLOAD =========
|
||||
BODY='{
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user